keystone_ui-preferences 0.6.0 → 0.7.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.
Files changed (23) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +146 -3
  3. data/app/controllers/concerns/keystone_ui/preferences/component_preferences.rb +36 -1
  4. data/app/javascript/keystone_ui/preferences/theme_settings_controller.js +59 -0
  5. data/app/models/keystone_ui/preferences/appearance_choice.rb +107 -0
  6. data/app/models/keystone_ui/preferences/chosen_colours.rb +39 -0
  7. data/app/models/keystone_ui/preferences/layout_choice.rb +6 -2
  8. data/app/models/keystone_ui/preferences/palette_css.rb +32 -0
  9. data/app/models/keystone_ui/preferences/pick_account_appearance.rb +54 -0
  10. data/app/models/keystone_ui/preferences/pick_appearance.rb +78 -0
  11. data/app/views/keystone_ui/preferences/settings/_account_appearance.html.erb +41 -0
  12. data/app/views/keystone_ui/preferences/settings/_appearance.html.erb +40 -0
  13. data/app/views/keystone_ui/preferences/settings/_colour_choices.html.erb +66 -0
  14. data/app/views/keystone_ui/preferences/settings/_look_choices.html.erb +8 -0
  15. data/lib/generators/keystone_ui/preferences/install/install_generator.rb +6 -1
  16. data/lib/generators/keystone_ui/preferences/update/templates/copy_keystone_ui_colors_theme_preferences.rb.erb +55 -0
  17. data/lib/generators/keystone_ui/preferences/update/update_generator.rb +15 -1
  18. data/lib/keystone_ui/preferences/colors_check.rb +21 -0
  19. data/lib/keystone_ui/preferences/configuration.rb +10 -1
  20. data/lib/keystone_ui/preferences/engine.rb +24 -0
  21. data/lib/keystone_ui/preferences/version.rb +1 -1
  22. data/lib/keystone_ui/preferences.rb +1 -0
  23. metadata +27 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4f636363927260121283af376ea874a464189878bc0a338840fe5c3a228b13dd
4
- data.tar.gz: c6e10c6b5e2b4442e74aa0448f7cbd68c23e292b5c21dfd7455291847c12e8c1
3
+ metadata.gz: bbf5cec0b85185ca3abee2d52c65c7e80f26044c9a0c7fb9a19df0fdba9464c6
4
+ data.tar.gz: a1885de65a350f4f7025fa2bdb1eb160a11d1f9402a4b38615672a4621a4d2bc
5
5
  SHA512:
6
- metadata.gz: c1ac4cd559b34527ff83fa954a793b8b9433d2c8caf7a76bfb581b8417ad6f6e9bc1c69c1e91b3aaece6466b5529b8124d6466e050e67022b7aef1ecc97a7ffd
7
- data.tar.gz: f8d94f6d19ce5788906d4eb8ab48c827bc38f631e5e1f3f2b7325931c2d83671efd6d03e6b610ddcb1dbd641599667a3da11050bc9350f32a4fcaaf70a38654c
6
+ metadata.gz: 834b8955f9a2048c833372321b52c13829376dc0d906e4c66d30bd4ef5d9b356f894057f6b1005c166d5b72e9fe33f1df7556f52bd6a9182e59613885c24771e
7
+ data.tar.gz: 30eb7dbafec075ee43fd6d1cc80a1af79fa6b9ef8c3eb1f68ef250f12f7a1fb042f44b9b1dccdd86580a52ac3de5c5ed15e4495ad0c8476887ba2dfac1bf53b1
data/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # keystone_ui-preferences
2
2
 
3
- Per-user saved choices for Keystone UI components, such as a data table's hidden columns.
3
+ Per-user saved choices for Keystone UI components, such as a data table's hidden columns and the colours every page is drawn in.
4
4
 
5
5
  A page gives a `ui_data_table` a `key:`. This gem saves each signed-in person's layout for that key and hands it back to keystone_ui, so the table renders from it and shows a Columns menu that saves to it. A person with nothing saved sees the hidden columns the table's own call names.
6
6
 
7
7
  ## Installation
8
8
 
9
- Add the gem and run its install generator, which copies a migration for its table:
9
+ Add the gem and run its install generator, which copies a migration for its table and the template picker script the appearance section uses:
10
10
 
11
11
  ```ruby
12
12
  gem "keystone_ui-preferences"
@@ -18,7 +18,14 @@ bin/rails generate keystone_ui:preferences:install
18
18
  bin/rails db:migrate
19
19
  ```
20
20
 
21
- After updating the gem, run its update generator to copy any migrations added since it was installed:
21
+ The script lands at `app/javascript/controllers/keystone_ui/preferences/theme_settings_controller.js`. Register it in `app/javascript/controllers/index.js`:
22
+
23
+ ```js
24
+ import ThemeSettingsController from "./keystone_ui/preferences/theme_settings_controller"
25
+ application.register("keystone-ui--preferences--theme-settings", ThemeSettingsController)
26
+ ```
27
+
28
+ After updating the gem, run its update generator to copy any migrations added since it was installed, and the template picker script when the app does not have it yet:
22
29
 
23
30
  ```bash
24
31
  bin/rails generate keystone_ui:preferences:update
@@ -49,6 +56,15 @@ KeystoneUi::Preferences.configure do |config|
49
56
  config.authentication_method = :authenticate_user! # runs before every save
50
57
  config.current_account_method = :current_account # returns the account; nil (the default) skips the account level
51
58
  config.max_value_bytes = 10_000 # the largest saved value accepted, in bytes
59
+ config.default_accent = "blue" # the accent for a person with no colours saved
60
+ config.default_surface = "zinc" # the surface for a person with no colours saved
61
+ config.default_background = "#ffffff" # the custom background for a person with no colours saved
62
+ config.default_text = "#18181b" # the custom text colour for a person with no colours saved
63
+ config.default_mode = "light" # the mode for a person with no mode saved and for a visitor
64
+ config.user_looks = true # whether a person may choose their own look
65
+ config.app_look_label = "Classic" # the label for the app's own look in the appearance section
66
+ config.account_colors = true # whether an account may choose its colours, and its members their own
67
+ config.account_looks = true # whether an account may choose its look
52
68
  end
53
69
  ```
54
70
 
@@ -133,6 +149,133 @@ A tab declared outside any group with keystone_ui's `navigation_tab` is one row
133
149
 
134
150
  Saving keeps `"order": [{ "group": "Admin" }, { "group": "Sales", "tabs": ["orders", "quotes"] }, { "group": "Console" }]` for that person under the `navigation` key, naming groups by label and tabs by key, and a tab declared outside any group by its label alone. It leaves their saved placement as it was. keystone_ui draws the navigation in that order on the next page they open, with anything the order does not name after it in its declared order. Reset removes the saved order, so the navigation goes back to the declared order. An order that is not a list of groups is refused with a message, and nothing is saved.
135
151
 
152
+ ## Colours
153
+
154
+ Add the palette style tag to your layout's `<head>`:
155
+
156
+ ```erb
157
+ <%= keystone_palette_style_tag %>
158
+ ```
159
+
160
+ It writes the same CSS variables keystone_ui-colors writes: `--color-accent-50` to `--color-accent-950`, `--color-surface-50` to `--color-surface-950`, `--color-custom-background` and `--color-custom-text`. A layout that already calls it for keystone_ui-colors keeps working unchanged, and an app that loads both gems stops at boot.
161
+
162
+ The colours come from the `colours` key, read the way a table's layout is read: the person's own row, or the account's row when the account does not let members choose. A saved value looks like `{ "accent": "#e11d48", "surface": "#f5e6c8", "text": "#3b2f1e" }` or `{ "template_name": "forest" }`. A named accent or surface, such as `emerald` or `stone`, uses that palette's shades from keystone_ui-looks, and a hex value has its shades generated. A custom hex surface is also the custom background. A saved accent or surface that is neither a name of lowercase letters, digits and underscores nor a six-digit hex, and a saved text colour that is not a six-digit hex, is drawn as the configured default for that colour instead.
163
+
164
+ A saved `template_name` draws that template's accent, surface, background and text from keystone_ui-looks, whatever else the value holds. The template named `default` is built from the four defaults in the configuration. A person with nothing saved, and a visitor who is not signed in, see those four defaults.
165
+
166
+ A saved name keystone_ui-looks no longer has still draws a page. A missing template draws the account's colours, or the four defaults when the account has none or its own template is the missing one. A missing accent or surface name draws the default accent or surface. Each of these writes one warning to the app's log naming the owner's type and id and the missing name, for example `keystone_ui-preferences: User 5 saved the template "retired", which keystone_ui-looks does not have`. A saved value that is not a name or a six-digit hex falls back with no warning. The saved row is left as it is until the owner saves again, and the appearance section shows no template selected for it.
167
+
168
+ ## Appearance
169
+
170
+ A person picks their colours in a settings_hub section the app registers:
171
+
172
+ ```ruby
173
+ SettingsHub.section :appearance, area: :user, title: "Appearance",
174
+ renders: "keystone_ui/preferences/settings/appearance",
175
+ runs: "KeystoneUi::Preferences::PickAppearance"
176
+ ```
177
+
178
+ The section looks and behaves as keystone_ui-colors' colour settings do. It offers Default, the five named templates from keystone_ui-looks and Custom, with an accent, a surface and a text colour picker under them. The person's own saved choice is selected, or Default when they have none, and the pickers show the colours drawn for them now. Picking a template sets the pickers to that template's colours at once, and changing a picker selects Custom, through the template picker script the install generator copies. Without the script the section still saves, and the form saves with a full page load, so it works in a Hotwire Native web view.
179
+
180
+ Saving a template keeps `{ "template_name": "forest" }` for that person under the `colours` key. Saving Custom keeps `{ "accent": "#e11d48", "surface": "#f5e6c8", "text": "#3b2f1e" }`. Saving Default removes the person's `colours` row, so the account's colours, or the app's defaults, apply again. The next page the person opens is drawn in the saved colours.
181
+
182
+ A template that is not Default or one of the five is refused with a message, and so is a Custom accent or surface that is neither a six-digit hex nor a palette name from keystone_ui-looks, and a text colour that is not a six-digit hex. Nothing is saved.
183
+
184
+ The `colours` key follows the account layouts above. While an account turns off members-choose for it, the section says the account sets the colours for everyone and offers no colours, and a save that sends colours anyway is refused with a message and saves nothing.
185
+
186
+ ## Mode
187
+
188
+ The appearance section also offers Light, Dark and System, as keystone_ui-colors' mode control does, with the person's saved mode selected, or `default_mode` when they have none. It offers Custom too when the person may choose their own colours, or when the colours that apply to them draw a background other than white. The mode is offered while the account sets the colours, and saving it there keeps the mode alone.
189
+
190
+ Saving keeps `{ "mode": "dark" }` for that person under the `mode` key. A mode is only ever read from the person's own row, never from the account's, so it applies whether or not the account lets members choose colours. A mode other than `light`, `dark`, `system` or `custom` is refused with a message, and nothing is saved.
191
+
192
+ The gem gives keystone_ui the mode through keystone_ui's theme mode supplier, so a layout whose `<html>` tag calls keystone_ui's `keystone_theme_attributes` is marked with the person's mode on every page:
193
+
194
+ ```erb
195
+ <html <%= keystone_theme_attributes %>>
196
+ ```
197
+
198
+ A visitor who is not signed in gets `default_mode`. A choice made with keystone_ui's theme toggle is kept in a cookie and wins over the saved mode in that browser. Saving in the appearance section clears that cookie, so the saved mode shows on the next page. The clearing runs in settings_hub's `after_settings_change` hook, which the `ComponentPreferences` concern defines, so an app that defines its own `after_settings_change` calls `super` from it.
199
+
200
+ ## Look
201
+
202
+ When the app registers looks with keystone_ui, the appearance section also offers a look, as keystone_ui-colors' look control does. It lists the app's own look first, labelled with `app_look_label`, and then every registered look by name. The person's saved look is selected, or the app's own look when they have none.
203
+
204
+ Saving a look keeps `{ "look": "rounded" }` for that person under the `look` key. Saving the app's own look removes the person's `look` row, so the account's look, or the app's default look, applies again. A look name keystone_ui does not register is refused with a message, and nothing is saved.
205
+
206
+ A person's look comes from their own row while the account lets members choose their own look, then from the account's row, and a saved look the app no longer registers is passed over for the next one. With neither, keystone_ui uses its `default_look`. A visitor who is not signed in gets `default_look` too.
207
+
208
+ The gem gives keystone_ui the look through keystone_ui's look supplier, so the same `keystone_theme_attributes` call marks the `<html>` tag with `data-look` on every page.
209
+
210
+ An app that keeps people off their own looks sets `user_looks` to false. The appearance section then shows no look choice, a save ignores any look sent with it and keeps the rest, and a person's saved look is passed over for the account's look or the app's default look. With no looks registered the section shows no look choice.
211
+
212
+ ## Account appearance
213
+
214
+ An admin sets the account's colours and look in a settings_hub section. Who may open it is the app's decision, so register it behind a capability of the app's choosing:
215
+
216
+ ```ruby
217
+ SettingsHub.section :account_appearance, area: :account, title: "Appearance",
218
+ capability: :manage_account,
219
+ renders: "keystone_ui/preferences/settings/account_appearance",
220
+ runs: "KeystoneUi::Preferences::PickAccountAppearance"
221
+ ```
222
+
223
+ The section looks and behaves as keystone_ui-colors' account picker does. It offers Default, the five named templates and Custom with the three colour pickers, then the app's own look and every registered look, with the account's saved choices selected, or Default and the app's own look when it has none. Under them are two switches, one for whether members choose their own colours and one for whether members choose their own look. Both are on until an admin turns them off.
224
+
225
+ Saving keeps the account's colours under the `colours` key, as `{ "template_name": "forest" }` or `{ "accent": "#e11d48", "surface": "#f5e6c8", "text": "#3b2f1e" }`, and its look under the `look` key, with each switch in that row's `members_choose` column. Saving Default keeps `{ "template_name": "default" }`, and saving the app's own look keeps an empty look, so neither removes the row its switch is on. The colours and look pass the same checks as a person's, and a template, colour or look that fails one is refused with a message, and nothing is saved. A save with no account is refused too.
226
+
227
+ While the colours switch is off, every member sees the account's colours, and their appearance section says the account sets the colours and offers no colour choice. While the look switch is off, every member sees the account's look, and their appearance section offers no look choice. A look a member sends then is ignored, and the rest of the save is kept. While the look switch is on, a member who chose a look keeps it, and a member with none sees the account's.
228
+
229
+ An app that keeps accounts off colours sets `account_colors` to false. Neither section then offers a colour choice or the colours switch, every page is drawn in the four configured defaults, and colours sent to either section are refused with a message. An app that keeps accounts off looks sets `account_looks` to false. The account's section then offers no look choice and no look switch, a look sent to it is ignored, and a saved account look is not applied.
230
+
231
+ ## Keeping a page on the app's colours
232
+
233
+ A controller that declares `keystone_host_colors` draws its pages in the app's four default colours, `default_mode` and keystone_ui's `default_look`, whatever the signed-in person or their account saved. The declaration has the same name keystone_ui-colors gives it, so a controller that already declares it keeps working unchanged. It takes the same `only:` and `except:` options as `before_action`.
234
+
235
+ ```ruby
236
+ class HomeController < ApplicationController
237
+ keystone_host_colors
238
+ end
239
+ ```
240
+
241
+ A choice made with keystone_ui's theme toggle in that browser still sets light or dark on these pages.
242
+
243
+ ## Moving off keystone_ui-colors
244
+
245
+ An app that saved colours, modes and looks with keystone_ui-colors keeps them by following these steps in this order. An app that has never installed keystone_ui-preferences adds it to the Gemfile and runs its install generator before step 1.
246
+
247
+ 1. Update keystone_ui-preferences.
248
+ 2. Run its update generator, which copies the migration below into `db/migrate`.
249
+ 3. Remove keystone_ui-colors from the Gemfile and run `bundle install`.
250
+ 4. Change every `KeystoneUi::Colors` name the app uses to its replacement. keystone_ui-colors' README lists each name and what replaces it under [Moving to keystone_ui-preferences](https://github.com/DYB-Development/keystone_ui-colors#moving-to-keystone_ui-preferences).
251
+ 5. Register the person's appearance section and the account's appearance section, the account's behind `capability: :manage_account`, as [Appearance](#appearance) and [Account appearance](#account-appearance) show.
252
+ 6. Call `super` from any `after_settings_change` the app defines.
253
+ 7. Run `bin/rails db:migrate`.
254
+
255
+ ```bash
256
+ bundle update keystone_ui-preferences
257
+ bin/rails generate keystone_ui:preferences:update
258
+ # remove keystone_ui-colors from the Gemfile
259
+ bundle install
260
+ # change each KeystoneUi::Colors name, register both appearance sections, call super from after_settings_change
261
+ bin/rails db:migrate
262
+ ```
263
+
264
+ Boot stops with `KeystoneUi::Preferences::ColorsCheck::Error` while keystone_ui-colors is loaded, and the message names keystone_ui-colors as the gem to remove and the update generator as the step to run first. `bin/rails db:migrate` boots the app, so keystone_ui-colors comes out of the Gemfile before the migration runs, and the generator runs before keystone_ui-colors comes out so the copy migration is in `db/migrate` when it does. An initializer, route, controller or helper that still names `KeystoneUi::Colors`, such as a `KeystoneUi::Colors.configure` block, a route mounting `KeystoneUi::Colors::Engine`, `include KeystoneUi::Colors::CurrentPalette` or `helper KeystoneUi::Colors::PaletteHelper`, also stops the app from booting, so every name is changed before the migration runs.
265
+
266
+ The generator copies a migration that reads every row of `keystone_ui_colors_theme_preferences` and writes up to three rows for the same owner:
267
+
268
+ | keystone_ui-colors | keystone_ui-preferences |
269
+ |--------------------|-------------------------|
270
+ | `template_name`, including `default` | `colours` row holding `{"template_name": ...}` |
271
+ | `accent`, `surface` and `text` with no template | `colours` row holding `{"accent": ..., "surface": ..., "text": ...}` |
272
+ | `members_choose` | `members_choose` of the `colours` row |
273
+ | `mode` | `mode` row holding `{"mode": ...}`, written only when a mode was saved |
274
+ | `look` | `look` row holding `{"look": ...}`, written only when a look was saved |
275
+ | `members_choose_look` | `members_choose` of the `look` row, which holds `{}` for an account on the app's own look that sets the look for everyone |
276
+
277
+ An owner and key that already has a row keeps it, so a choice made in keystone_ui-preferences is never replaced by an older one, and running the migration again changes nothing. In an app without keystone_ui-colors' table the migration does nothing. Rolling it back changes neither table. Drop keystone_ui-colors' table once the copy has run.
278
+
136
279
  ## What is stored
137
280
 
138
281
  One row per owner and component key, in `keystone_ui_preferences_component_preferences`:
@@ -8,7 +8,13 @@ module KeystoneUi
8
8
  extend ActiveSupport::Concern
9
9
 
10
10
  included do
11
- helper_method :keystone_component_preference if respond_to?(:helper_method)
11
+ helper_method :keystone_component_preference, :keystone_palette_style_tag, :keystone_theme_mode, :keystone_look if respond_to?(:helper_method)
12
+ end
13
+
14
+ class_methods do
15
+ def keystone_host_colors(**options)
16
+ before_action :keystone_keep_host_colours, **options
17
+ end
12
18
  end
13
19
 
14
20
  def keystone_component_preference(component_key)
@@ -22,6 +28,19 @@ module KeystoneUi
22
28
  }
23
29
  end
24
30
 
31
+ def keystone_palette_style_tag
32
+ colours = keystone_appearance_choice.colours
33
+ helpers.content_tag(:style, PaletteCss.new(**colours).to_s)
34
+ end
35
+
36
+ def keystone_theme_mode
37
+ keystone_appearance_choice.mode
38
+ end
39
+
40
+ def keystone_look
41
+ keystone_appearance_choice.look
42
+ end
43
+
25
44
  def keystone_layout_choice(component_key)
26
45
  LayoutChoice.new(person: send(KeystoneUi::Preferences.configuration.current_owner_method), account: keystone_preferences_account, component_key: component_key)
27
46
  end
@@ -30,6 +49,22 @@ module KeystoneUi
30
49
  method = KeystoneUi::Preferences.configuration.current_account_method
31
50
  send(method) if method
32
51
  end
52
+
53
+ private
54
+
55
+ def keystone_keep_host_colours
56
+ @keystone_host_colours = true
57
+ end
58
+
59
+ def keystone_appearance_choice
60
+ return AppearanceChoice.new(person: nil, account: nil) if @keystone_host_colours
61
+
62
+ AppearanceChoice.new(person: send(KeystoneUi::Preferences.configuration.current_owner_method), account: keystone_preferences_account)
63
+ end
64
+
65
+ def after_settings_change(section:, **)
66
+ cookies.delete(KeystoneUi::ThemeChoice::COOKIE) if section.runs.to_s == PickAppearance.name
67
+ end
33
68
  end
34
69
  end
35
70
  end
@@ -0,0 +1,59 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["accentPicker", "surfacePicker", "textPicker", "customRadio", "templateLabel"]
5
+
6
+ connect() {
7
+ this.colorPickerTargets().forEach(picker => picker.addEventListener("change", () => this.customColorChanged()))
8
+ }
9
+
10
+ colorPickerTargets() {
11
+ return [
12
+ this.hasAccentPickerTarget && this.accentPickerTarget,
13
+ this.hasSurfacePickerTarget && this.surfacePickerTarget,
14
+ this.hasTextPickerTarget && this.textPickerTarget
15
+ ].filter(Boolean)
16
+ }
17
+
18
+ selectTemplate(event) {
19
+ const input = event.currentTarget
20
+ const label = input.closest("[data-template]")
21
+ const accentHex = input.dataset.accentHex
22
+ const surfaceHex = input.dataset.surfaceHex
23
+ const textHex = input.dataset.textHex
24
+
25
+ this.setPickerValue(this.accentPickerTarget, accentHex)
26
+ this.setPickerValue(this.surfacePickerTarget, surfaceHex)
27
+ this.setPickerValue(this.textPickerTarget, textHex)
28
+
29
+ this.highlightTemplate(label.dataset.template)
30
+ }
31
+
32
+ customColorChanged() {
33
+ if (this.hasCustomRadioTarget) this.customRadioTarget.checked = true
34
+ this.highlightTemplate("custom")
35
+ }
36
+
37
+ highlightTemplate(selected) {
38
+ this.templateLabelTargets.forEach(label => {
39
+ const border = label.querySelector("span")
40
+ if (label.dataset.template === selected) {
41
+ border.classList.remove("border-transparent")
42
+ border.classList.add("border-accent-500")
43
+ } else {
44
+ border.classList.remove("border-accent-500")
45
+ border.classList.add("border-transparent")
46
+ }
47
+ })
48
+ }
49
+
50
+ setPickerValue(wrapper, hex) {
51
+ const input = wrapper.querySelector("input[type='hidden']")
52
+ const swatch = wrapper.querySelector("[data-color-picker-target='swatch']")
53
+ const hexLabel = wrapper.querySelector("[data-color-picker-target='hexLabel']")
54
+
55
+ if (input) input.value = hex
56
+ if (swatch) swatch.style.backgroundColor = hex
57
+ if (hexLabel) hexLabel.textContent = hex
58
+ }
59
+ }
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeystoneUi
4
+ module Preferences
5
+ class AppearanceChoice
6
+ DEFAULT_TEMPLATE = "default"
7
+ HEX = /\A#\h{6}\z/
8
+ PALETTE_NAME = /\A[a-z][a-z0-9_]*\z/
9
+ WHITE = "#ffffff"
10
+
11
+ def initialize(person:, account:)
12
+ @person = person
13
+ @account = account
14
+ end
15
+
16
+ def colours
17
+ @colours ||= chosen_colours
18
+ end
19
+
20
+ def mode
21
+ LayoutChoice.new(person: @person, account: nil, component_key: "mode").value.to_h["mode"] || configuration.default_mode
22
+ end
23
+
24
+ def look
25
+ return nil unless @person
26
+
27
+ [ (saved_look(@person) if person_chooses_look?), (saved_look(@account) if configuration.account_looks) ].find { |look| KeystoneUi.configuration.looks.key?(look) }
28
+ end
29
+
30
+ def person_chooses_look?
31
+ configuration.user_looks && LayoutChoice.new(person: @person, account: @account, component_key: "look").members_choose?
32
+ end
33
+
34
+ def custom_offered?
35
+ !colours[:background].casecmp?(WHITE)
36
+ end
37
+
38
+ private
39
+
40
+ def chosen_colours
41
+ return default_colours unless configuration.account_colors
42
+ return default_colours if saved["template_name"] == DEFAULT_TEMPLATE
43
+ return template_colours if template_offered?
44
+ return fallback_colours if saved["template_name"].present?
45
+
46
+ surface = catalogue_colour("surface", KeystoneUi::Looks::Palettes::SURFACES) || configuration.default_surface
47
+ {
48
+ accent: catalogue_colour("accent", KeystoneUi::Looks::Palettes::ACCENTS) || configuration.default_accent,
49
+ surface: surface,
50
+ background: surface.start_with?("#") ? surface : configuration.default_background,
51
+ text: (saved["text"] if saved["text"].to_s.match?(HEX)) || configuration.default_text
52
+ }
53
+ end
54
+
55
+ def saved_look(owner)
56
+ LayoutChoice.new(person: owner, account: nil, component_key: "look").value.to_h["look"]
57
+ end
58
+
59
+ def catalogue_colour(kind, palettes)
60
+ value = saved[kind].to_s
61
+ return value if value.match?(HEX)
62
+ return unless value.match?(PALETTE_NAME)
63
+ return value if palettes.key?(value.to_sym)
64
+
65
+ warn_missing(kind, value)
66
+ nil
67
+ end
68
+
69
+ def default_colours
70
+ { accent: configuration.default_accent, surface: configuration.default_surface, background: configuration.default_background, text: configuration.default_text }
71
+ end
72
+
73
+ def fallback_colours
74
+ warn_missing("template", saved["template_name"])
75
+ return default_colours if @account.nil? || colours_choice.preference.owned_by?(@account)
76
+
77
+ AppearanceChoice.new(person: @account, account: nil).colours
78
+ end
79
+
80
+ def warn_missing(kind, name)
81
+ row = colours_choice.preference
82
+ Rails.logger.warn("keystone_ui-preferences: #{row.owner_type} #{row.owner_id} saved the #{kind} #{name.to_s.inspect}, which keystone_ui-looks does not have")
83
+ end
84
+
85
+ def template_offered?
86
+ KeystoneUi::Looks::Templates.names.map(&:to_s).include?(saved["template_name"].to_s)
87
+ end
88
+
89
+ def template_colours
90
+ template = KeystoneUi::Looks::Templates[saved["template_name"]]
91
+ { accent: template[:accent].to_s, surface: template[:surface].to_s, background: template[:background], text: template[:text] }
92
+ end
93
+
94
+ def saved
95
+ @saved ||= colours_choice.value.to_h
96
+ end
97
+
98
+ def colours_choice
99
+ @colours_choice ||= LayoutChoice.new(person: @person, account: @account, component_key: "colours")
100
+ end
101
+
102
+ def configuration
103
+ KeystoneUi::Preferences.configuration
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeystoneUi
4
+ module Preferences
5
+ class ChosenColours
6
+ def initialize(values)
7
+ @values = values
8
+ end
9
+
10
+ def refusal
11
+ return Refusal.new("Choose Default, one of the colour templates or Custom.") unless template_offered?
12
+
13
+ Refusal.new("Choose each colour as a six-digit hex value, such as #1d4ed8.") unless colours_offered?
14
+ end
15
+
16
+ def value
17
+ return { "template_name" => @values[:template_name] } if @values[:template_name].present?
18
+
19
+ { "accent" => @values[:accent], "surface" => @values[:surface], "text" => @values[:text] }
20
+ end
21
+
22
+ private
23
+
24
+ def template_offered?
25
+ @values[:template_name].blank? || [ AppearanceChoice::DEFAULT_TEMPLATE, *KeystoneUi::Looks::Templates.names.map(&:to_s) ].include?(@values[:template_name])
26
+ end
27
+
28
+ def colours_offered?
29
+ return true if @values[:template_name].present?
30
+
31
+ colour_offered?(@values[:accent], KeystoneUi::Looks::Palettes::ACCENTS) && colour_offered?(@values[:surface], KeystoneUi::Looks::Palettes::SURFACES) && colour_offered?(@values[:text], {})
32
+ end
33
+
34
+ def colour_offered?(colour, palettes)
35
+ colour.to_s.match?(AppearanceChoice::HEX) || palettes.key?(colour.to_s.to_sym)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -12,9 +12,13 @@ module KeystoneUi
12
12
  end
13
13
 
14
14
  def value
15
- return account_preference.value unless members_choose?
15
+ preference&.value
16
+ end
17
+
18
+ def preference
19
+ return account_preference unless members_choose?
16
20
 
17
- (own_preference || account_preference)&.value
21
+ own_preference || account_preference
18
22
  end
19
23
 
20
24
  def members_choose?
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeystoneUi
4
+ module Preferences
5
+ class PaletteCss
6
+ def initialize(accent:, surface:, background:, text:)
7
+ @accent = accent
8
+ @surface = surface
9
+ @background = background
10
+ @text = text
11
+ end
12
+
13
+ def to_s
14
+ lines = shade_lines("accent", shades(@accent) { KeystoneUi::Looks::Palettes.accent(@accent) }) + shade_lines("surface", shades(@surface) { KeystoneUi::Looks::Palettes.surface(@surface) })
15
+ lines << " --color-custom-background: #{@background};" << " --color-custom-text: #{@text};"
16
+ ":root {\n#{lines.join("\n")}\n}"
17
+ end
18
+
19
+ private
20
+
21
+ def shades(colour)
22
+ return KeystoneUi::Looks::Palettes.generate_shades(colour) if colour.start_with?("#")
23
+
24
+ yield
25
+ end
26
+
27
+ def shade_lines(name, shades)
28
+ shades.map { |shade, hex| " --color-#{name}-#{shade}: #{hex};" }
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeystoneUi
4
+ module Preferences
5
+ class PickAccountAppearance
6
+ def initialize(values:, person: nil, account: nil)
7
+ @person = person
8
+ @account = account
9
+ @values = values
10
+ end
11
+
12
+ def call
13
+ return Refusal.new("There is no account to save this appearance on.") unless @account
14
+ return Refusal.new("Choose the app's own look or one of the looks it offers.") unless look_offered?
15
+
16
+ return Refusal.new("This app does not let accounts choose their colours.") if colours_sent? && !KeystoneUi::Preferences.configuration.account_colors
17
+
18
+ chosen = ChosenColours.new(@values) if colours_sent?
19
+ return chosen.refusal if chosen&.refusal
20
+
21
+ keep("colours", (chosen ? { value: chosen.value } : {}).merge(switch(:members_choose)))
22
+ keep_look
23
+ Kept.new
24
+ end
25
+
26
+ private
27
+
28
+ def colours_sent?
29
+ @values.key?(:template_name)
30
+ end
31
+
32
+ def look_offered?
33
+ !look_sent? || @values[:look].blank? || KeystoneUi.configuration.looks.key?(@values[:look])
34
+ end
35
+
36
+ def keep_look
37
+ chosen = look_sent? ? { value: { "look" => @values[:look].presence }.compact } : {}
38
+ keep("look", chosen.merge(switch(:members_choose_look)))
39
+ end
40
+
41
+ def look_sent?
42
+ @values.key?(:look) && KeystoneUi::Preferences.configuration.account_looks
43
+ end
44
+
45
+ def keep(component_key, changes)
46
+ ComponentPreference.find_or_initialize_by(owner: @account, component_key: component_key).update!(changes) if changes.any?
47
+ end
48
+
49
+ def switch(name)
50
+ @values.key?(name) ? { members_choose: @values[name] == "1" } : {}
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeystoneUi
4
+ module Preferences
5
+ class PickAppearance
6
+ def initialize(values:, person: nil, account: nil)
7
+ @person = person
8
+ @account = account
9
+ @values = values
10
+ end
11
+
12
+ def call
13
+ return Refusal.new("Choose Light, Dark, System or Custom for the mode.") unless mode_offered?
14
+ return Refusal.new("Choose the app's own look or one of the looks it offers.") unless look_offered?
15
+ return keep_mode_and_look unless @values.key?(:template_name)
16
+ return Refusal.new("This app sets the colours for everyone, so this was not saved.") unless KeystoneUi::Preferences.configuration.account_colors
17
+ return Refusal.new("Your account sets the colours for everyone, so this was not saved.") unless members_choose?
18
+ return use_default if @values[:template_name] == AppearanceChoice::DEFAULT_TEMPLATE
19
+
20
+ chosen = ChosenColours.new(@values)
21
+ return chosen.refusal if chosen.refusal
22
+
23
+ preference.update!(value: chosen.value)
24
+ keep_mode_and_look
25
+ end
26
+
27
+ private
28
+
29
+ def mode_offered?
30
+ !@values.key?(:mode) || KeystoneUi::ThemeChoice::MODES.include?(@values[:mode])
31
+ end
32
+
33
+ def look_offered?
34
+ !look_sent? || @values[:look].blank? || KeystoneUi.configuration.looks.key?(@values[:look])
35
+ end
36
+
37
+ def members_choose?
38
+ LayoutChoice.new(person: @person, account: @account, component_key: "colours").members_choose?
39
+ end
40
+
41
+ def use_default
42
+ preference.destroy! if preference.persisted?
43
+ keep_mode_and_look
44
+ end
45
+
46
+ def keep_mode_and_look
47
+ keep_mode
48
+ keep_look
49
+ Kept.new
50
+ end
51
+
52
+ def keep_mode
53
+ return unless @values.key?(:mode)
54
+
55
+ ComponentPreference.find_or_initialize_by(owner: @person, component_key: "mode").update!(value: { "mode" => @values[:mode] })
56
+ end
57
+
58
+ def keep_look
59
+ return unless look_sent?
60
+ return look_preference.update!(value: { "look" => @values[:look] }) if @values[:look].present?
61
+
62
+ look_preference.destroy! if look_preference.persisted?
63
+ end
64
+
65
+ def look_sent?
66
+ @values.key?(:look) && AppearanceChoice.new(person: @person, account: @account).person_chooses_look?
67
+ end
68
+
69
+ def look_preference
70
+ @look_preference ||= ComponentPreference.find_or_initialize_by(owner: @person, component_key: "look")
71
+ end
72
+
73
+ def preference
74
+ @preference ||= ComponentPreference.find_or_initialize_by(owner: @person, component_key: "colours")
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,41 @@
1
+ <%
2
+ colours = KeystoneUi::Preferences::ComponentPreference.find_by(owner: account, component_key: "colours")
3
+ saved = colours&.value.presence || { "template_name" => "default" }
4
+ look = KeystoneUi::Preferences::ComponentPreference.find_by(owner: account, component_key: "look")
5
+ offers_colours = KeystoneUi::Preferences.configuration.account_colors
6
+ offers_look = KeystoneUi::Preferences.configuration.account_looks && KeystoneUi.configuration.looks.any?
7
+ drawn = KeystoneUi::Preferences::AppearanceChoice.new(person: account, account: nil).colours
8
+ %>
9
+
10
+ <% if !offers_colours && !offers_look %>
11
+ <%= ui_alert(message: "This app does not let accounts choose their colours or look.") %>
12
+ <% else %>
13
+ <%= ui_panel(padding: :lg) do %>
14
+ <%= form_with(url: submit_url, method: :patch, data: { turbo: false, controller: "keystone-ui--preferences--theme-settings" }) do %>
15
+ <div style="display: flex; flex-direction: column; gap: 2rem;">
16
+ <% if offers_colours %>
17
+ <%= render partial: "keystone_ui/preferences/settings/colour_choices", locals: { saved: saved, drawn: drawn } %>
18
+ <% end %>
19
+
20
+ <% if offers_look %>
21
+ <%= render partial: "keystone_ui/preferences/settings/look_choices", locals: { saved_look: look&.value.to_h["look"] } %>
22
+ <% end %>
23
+
24
+ <%= ui_section(title: "Members", spacing: :sm) do %>
25
+ <% if offers_colours %>
26
+ <%= hidden_field_tag "members_choose", "0", id: nil %>
27
+ <%= ui_checkbox_row(name: "members_choose", value: "1", label: "Members choose their own colours", checked: colours.nil? || colours.members_choose) %>
28
+ <% end %>
29
+ <% if offers_look %>
30
+ <%= hidden_field_tag "members_choose_look", "0", id: nil %>
31
+ <%= ui_checkbox_row(name: "members_choose_look", value: "1", label: "Members choose their own look", checked: look.nil? || look.members_choose) %>
32
+ <% end %>
33
+ <% end %>
34
+
35
+ <div>
36
+ <%= ui_button(label: "Save", type: :submit) %>
37
+ </div>
38
+ </div>
39
+ <% end %>
40
+ <% end %>
41
+ <% end %>
@@ -0,0 +1,40 @@
1
+ <%
2
+ choice = KeystoneUi::Preferences::LayoutChoice.new(person: person, account: account, component_key: "colours")
3
+ configuration = KeystoneUi::Preferences.configuration
4
+ chooses_colours = configuration.account_colors && choice.members_choose?
5
+ appearance = KeystoneUi::Preferences::AppearanceChoice.new(person: person, account: account)
6
+ drawn = appearance.colours
7
+ saved = KeystoneUi::Preferences::ComponentPreference.find_by(owner: person, component_key: "colours")&.value || { "template_name" => "default" }
8
+ saved_look = KeystoneUi::Preferences::ComponentPreference.find_by(owner: person, component_key: "look")&.value.to_h["look"]
9
+ %>
10
+
11
+ <% if configuration.account_colors && !choice.members_choose? %>
12
+ <%= ui_alert(message: "Your account sets the colours for everyone.") %>
13
+ <% end %>
14
+
15
+ <%= ui_panel(padding: :lg) do %>
16
+ <%= form_with(url: submit_url, method: :patch, data: { turbo: false, controller: "keystone-ui--preferences--theme-settings" }) do %>
17
+ <div style="display: flex; flex-direction: column; gap: 2rem;">
18
+ <% if chooses_colours %>
19
+ <%= render partial: "keystone_ui/preferences/settings/colour_choices", locals: { saved: saved, drawn: drawn } %>
20
+ <% end %>
21
+
22
+ <% if appearance.person_chooses_look? && KeystoneUi.configuration.looks.any? %>
23
+ <%= render partial: "keystone_ui/preferences/settings/look_choices", locals: { saved_look: saved_look } %>
24
+ <% end %>
25
+
26
+ <%= ui_section(title: "Mode", spacing: :sm) do %>
27
+ <div style="display: flex; flex-wrap: wrap; gap: 0.5rem;">
28
+ <% KeystoneUi::ThemeChoice::MODES.each do |mode| %>
29
+ <% next if mode == "custom" && !(chooses_colours || appearance.custom_offered?) %>
30
+ <%= ui_radio_card(name: "mode", value: mode, label: mode.capitalize, checked: appearance.mode == mode) %>
31
+ <% end %>
32
+ </div>
33
+ <% end %>
34
+
35
+ <div>
36
+ <%= ui_button(label: "Save", type: :submit) %>
37
+ </div>
38
+ </div>
39
+ <% end %>
40
+ <% end %>
@@ -0,0 +1,66 @@
1
+ <%
2
+ configuration = KeystoneUi::Preferences.configuration
3
+ palette_hex = ->(colour, palette) { colour.to_s.start_with?("#") ? colour.to_s : palette.call(colour)[500] }
4
+ templates = { "default" => { accent: configuration.default_accent, surface: configuration.default_surface, text: configuration.default_text, label: "Default" } }
5
+ KeystoneUi::Looks::Templates.all.each { |name, template| templates[name.to_s] = template }
6
+ %>
7
+
8
+ <%= ui_section(title: "Theme", spacing: :sm) do %>
9
+ <div style="display: flex; flex-wrap: wrap; gap: 0.5rem;">
10
+ <% templates.each do |name, template| %>
11
+ <% accent_hex = palette_hex.call(template[:accent], KeystoneUi::Looks::Palettes.method(:accent)) %>
12
+ <% surface_hex = palette_hex.call(template[:surface], KeystoneUi::Looks::Palettes.method(:surface)) %>
13
+ <%= ui_option_card(
14
+ name: "template_name",
15
+ value: name,
16
+ selected: saved["template_name"] == name,
17
+ input_data: {
18
+ action: "keystone-ui--preferences--theme-settings#selectTemplate",
19
+ "accent-hex": accent_hex,
20
+ "surface-hex": surface_hex,
21
+ "text-hex": template[:text]
22
+ },
23
+ label_data: {
24
+ "keystone-ui--preferences--theme-settings-target": "templateLabel",
25
+ template: name
26
+ }
27
+ ) do %>
28
+ <span style="display: inline-block; width: 1rem; height: 1rem; border-radius: 9999px; background-color: <%= accent_hex %>;"></span>
29
+ <span style="display: inline-block; width: 1rem; height: 1rem; border-radius: 0.25rem; background-color: <%= surface_hex %>;"></span>
30
+ <span><%= template[:label] %></span>
31
+ <% end %>
32
+ <% end %>
33
+ <%= ui_option_card(
34
+ name: "template_name",
35
+ value: "",
36
+ selected: saved["template_name"].blank?,
37
+ input_data: {
38
+ "keystone-ui--preferences--theme-settings-target": "customRadio"
39
+ },
40
+ label_data: {
41
+ "keystone-ui--preferences--theme-settings-target": "templateLabel",
42
+ template: "custom"
43
+ }
44
+ ) do %>
45
+ <span>Custom</span>
46
+ <% end %>
47
+ </div>
48
+ <% end %>
49
+
50
+ <%= ui_section(title: "Accent Color", spacing: :sm) do %>
51
+ <div data-keystone-ui--preferences--theme-settings-target="accentPicker">
52
+ <%= ui_color_picker(name: "accent", value: palette_hex.call(drawn[:accent], KeystoneUi::Looks::Palettes.method(:accent))) %>
53
+ </div>
54
+ <% end %>
55
+
56
+ <%= ui_section(title: "Surface", spacing: :sm) do %>
57
+ <div data-keystone-ui--preferences--theme-settings-target="surfacePicker">
58
+ <%= ui_color_picker(name: "surface", value: palette_hex.call(drawn[:surface], KeystoneUi::Looks::Palettes.method(:surface))) %>
59
+ </div>
60
+ <% end %>
61
+
62
+ <%= ui_section(title: "Text Color", spacing: :sm) do %>
63
+ <div data-keystone-ui--preferences--theme-settings-target="textPicker">
64
+ <%= ui_color_picker(name: "text", value: drawn[:text]) %>
65
+ </div>
66
+ <% end %>
@@ -0,0 +1,8 @@
1
+ <%= ui_section(title: "Look", spacing: :sm) do %>
2
+ <div style="display: flex; flex-wrap: wrap; gap: 0.5rem;">
3
+ <%= ui_radio_card(name: "look", value: "", label: KeystoneUi::Preferences.configuration.app_look_label, checked: saved_look.nil?) %>
4
+ <% KeystoneUi.configuration.looks.each_key do |look| %>
5
+ <%= ui_radio_card(name: "look", value: look, label: look.humanize, checked: saved_look == look) %>
6
+ <% end %>
7
+ </div>
8
+ <% end %>
@@ -11,7 +11,7 @@ module KeystoneUi
11
11
 
12
12
  source_root File.expand_path("templates", __dir__)
13
13
 
14
- desc "Installs KeystoneUi::Preferences: copies its migration."
14
+ desc "Installs KeystoneUi::Preferences: copies its migration and its template picker script."
15
15
 
16
16
  def copy_migration
17
17
  migration_template(
@@ -19,6 +19,11 @@ module KeystoneUi
19
19
  "db/migrate/create_keystone_ui_preferences_component_preferences.rb"
20
20
  )
21
21
  end
22
+
23
+ def copy_template_picker_script
24
+ script = File.expand_path("../../../../../app/javascript/keystone_ui/preferences/theme_settings_controller.js", __dir__)
25
+ create_file "app/javascript/controllers/keystone_ui/preferences/theme_settings_controller.js", File.read(script)
26
+ end
22
27
  end
23
28
  end
24
29
  end
@@ -0,0 +1,55 @@
1
+ class CopyKeystoneUiColorsThemePreferences < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ class ThemePreference < ActiveRecord::Base
3
+ self.table_name = "keystone_ui_colors_theme_preferences"
4
+ end
5
+
6
+ class ComponentPreference < ActiveRecord::Base
7
+ self.table_name = "keystone_ui_preferences_component_preferences"
8
+ end
9
+
10
+ def up
11
+ return unless table_exists?(:keystone_ui_colors_theme_preferences)
12
+
13
+ ThemePreference.find_in_batches do |theme_preferences|
14
+ rows = theme_preferences.flat_map { |theme_preference| [ colours(theme_preference), mode(theme_preference), look(theme_preference) ].compact }
15
+ saved = saved_keys(theme_preferences)
16
+ missing = rows.reject { |row| saved.include?(row.values_at(:owner_type, :owner_id, :component_key)) }
17
+ ComponentPreference.insert_all(missing) if missing.any?
18
+ missing.each { |row| Rails.cache.delete([ "keystone_ui_preferences", *row.values_at(:owner_type, :owner_id, :component_key) ].join("/")) }
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def saved_keys(theme_preferences)
25
+ theme_preferences.group_by(&:owner_type).flat_map do |owner_type, owned|
26
+ ComponentPreference.where(owner_type: owner_type, owner_id: owned.map(&:owner_id), component_key: %w[colours mode look]).pluck(:owner_type, :owner_id, :component_key)
27
+ end.to_set
28
+ end
29
+
30
+ def colours(theme_preference)
31
+ row(theme_preference, "colours", colours_value(theme_preference), members_choose: theme_preference.members_choose)
32
+ end
33
+
34
+ def mode(theme_preference)
35
+ return nil if theme_preference.mode.blank?
36
+
37
+ row(theme_preference, "mode", { "mode" => theme_preference.mode })
38
+ end
39
+
40
+ def look(theme_preference)
41
+ return nil if theme_preference.look.blank? && theme_preference.members_choose_look
42
+
43
+ row(theme_preference, "look", { "look" => theme_preference.look.presence }.compact, members_choose: theme_preference.members_choose_look)
44
+ end
45
+
46
+ def colours_value(theme_preference)
47
+ return { "template_name" => theme_preference.template_name } if theme_preference.template_name.present?
48
+
49
+ { "accent" => theme_preference.accent, "surface" => theme_preference.surface, "text" => theme_preference.text }
50
+ end
51
+
52
+ def row(theme_preference, component_key, value, members_choose: true)
53
+ { owner_type: theme_preference.owner_type, owner_id: theme_preference.owner_id, component_key: component_key, value: value, members_choose: members_choose }
54
+ end
55
+ end
@@ -11,7 +11,7 @@ module KeystoneUi
11
11
 
12
12
  source_root File.expand_path("templates", __dir__)
13
13
 
14
- desc "Updates KeystoneUi::Preferences: copies migrations added since it was installed."
14
+ desc "Updates KeystoneUi::Preferences: copies migrations added since it was installed and the template picker script when the app has none."
15
15
 
16
16
  def copy_remove_extra_owner_index_migration
17
17
  migration_template(
@@ -19,6 +19,20 @@ module KeystoneUi
19
19
  "db/migrate/remove_extra_owner_index_from_keystone_ui_preferences_component_preferences.rb"
20
20
  )
21
21
  end
22
+
23
+ def copy_keystone_ui_colors_theme_preferences_migration
24
+ migration_template(
25
+ "copy_keystone_ui_colors_theme_preferences.rb.erb",
26
+ "db/migrate/copy_keystone_ui_colors_theme_preferences.rb"
27
+ )
28
+ end
29
+
30
+ def copy_template_picker_script
31
+ copy = "app/javascript/controllers/keystone_ui/preferences/theme_settings_controller.js"
32
+ return if File.exist?(File.join(destination_root, copy))
33
+
34
+ create_file copy, File.read(File.expand_path("../../../../../app/javascript/keystone_ui/preferences/theme_settings_controller.js", __dir__))
35
+ end
22
36
  end
23
37
  end
24
38
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeystoneUi
4
+ module Preferences
5
+ class ColorsCheck
6
+ class Error < StandardError; end
7
+
8
+ MESSAGE = "keystone_ui-preferences and keystone_ui-colors both save colours, modes and looks, so the app cannot load both. " \
9
+ "Run bin/rails generate keystone_ui:preferences:update first, so the migration copying keystone_ui-colors' saved choices is in db/migrate, " \
10
+ "then remove keystone_ui-colors from the Gemfile and run bin/rails db:migrate."
11
+
12
+ def initialize(colors_loaded:)
13
+ @colors_loaded = colors_loaded
14
+ end
15
+
16
+ def call
17
+ raise Error, MESSAGE if @colors_loaded
18
+ end
19
+ end
20
+ end
21
+ end
@@ -3,13 +3,22 @@
3
3
  module KeystoneUi
4
4
  module Preferences
5
5
  class Configuration
6
- attr_accessor :current_owner_method, :authentication_method, :current_account_method, :max_value_bytes
6
+ attr_accessor :current_owner_method, :authentication_method, :current_account_method, :max_value_bytes, :default_accent, :default_surface, :default_background, :default_text, :default_mode, :user_looks, :app_look_label, :account_colors, :account_looks
7
7
 
8
8
  def initialize
9
9
  @current_owner_method = :current_user
10
10
  @authentication_method = :authenticate_user!
11
11
  @component_names = {}
12
12
  @max_value_bytes = 10_000
13
+ @default_accent = "blue"
14
+ @default_surface = "zinc"
15
+ @default_background = "#ffffff"
16
+ @default_text = "#18181b"
17
+ @default_mode = "light"
18
+ @user_looks = true
19
+ @app_look_label = "Classic"
20
+ @account_colors = true
21
+ @account_looks = true
13
22
  end
14
23
 
15
24
  def name_component(component_key, name)
@@ -1,10 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "keystone_ui/preferences/colors_check"
4
+
3
5
  module KeystoneUi
4
6
  module Preferences
5
7
  class Engine < ::Rails::Engine
6
8
  isolate_namespace KeystoneUi::Preferences
7
9
 
10
+ def self.check_colors
11
+ KeystoneUi::Preferences::ColorsCheck.new(colors_loaded: KeystoneUi.const_defined?(:Colors, false)).call
12
+ end
13
+
14
+ config.after_initialize do
15
+ KeystoneUi::Preferences::Engine.check_colors
16
+ end
17
+
8
18
  initializer "keystone_ui.preferences.supplier" do
9
19
  KeystoneUi.configure do |config|
10
20
  config.preference_supplier = lambda do |view, component_key|
@@ -12,6 +22,20 @@ module KeystoneUi
12
22
  end
13
23
  end
14
24
  end
25
+
26
+ initializer "keystone_ui.preferences.theme_mode" do
27
+ KeystoneUi.configure do |config|
28
+ config.theme_mode_supplier = lambda do |view|
29
+ view.respond_to?(:keystone_theme_mode) ? view.keystone_theme_mode : KeystoneUi::Preferences.configuration.default_mode
30
+ end
31
+ end
32
+ end
33
+
34
+ initializer "keystone_ui.preferences.look" do
35
+ KeystoneUi.configure do |config|
36
+ config.look_supplier = ->(view) { view.keystone_look if view.respond_to?(:keystone_look) }
37
+ end
38
+ end
15
39
  end
16
40
  end
17
41
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module KeystoneUi
4
4
  module Preferences
5
- VERSION = "0.6.0"
5
+ VERSION = "0.7.0"
6
6
  end
7
7
  end
@@ -1,5 +1,6 @@
1
1
  require "keystone_ui"
2
2
  require "keystone_ui/navigation_order"
3
+ require "keystone_ui/looks"
3
4
  require "keystone_ui/preferences/version"
4
5
  require "keystone_ui/preferences/configuration"
5
6
  require "keystone_ui/preferences/engine"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: keystone_ui-preferences
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tyler Schneider
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: 0.41.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: keystone_ui-looks
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 0.3.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 0.3.0
26
40
  - !ruby/object:Gem::Dependency
27
41
  name: railties
28
42
  requirement: !ruby/object:Gem::Requirement
@@ -79,25 +93,37 @@ files:
79
93
  - app/controllers/concerns/keystone_ui/preferences/component_preferences.rb
80
94
  - app/controllers/keystone_ui/preferences/application_controller.rb
81
95
  - app/controllers/keystone_ui/preferences/component_preferences_controller.rb
96
+ - app/javascript/keystone_ui/preferences/theme_settings_controller.js
97
+ - app/models/keystone_ui/preferences/appearance_choice.rb
82
98
  - app/models/keystone_ui/preferences/application_record.rb
99
+ - app/models/keystone_ui/preferences/chosen_colours.rb
83
100
  - app/models/keystone_ui/preferences/component_preference.rb
84
101
  - app/models/keystone_ui/preferences/kept.rb
85
102
  - app/models/keystone_ui/preferences/layout_choice.rb
86
103
  - app/models/keystone_ui/preferences/navigation_arrangement.rb
104
+ - app/models/keystone_ui/preferences/palette_css.rb
105
+ - app/models/keystone_ui/preferences/pick_account_appearance.rb
87
106
  - app/models/keystone_ui/preferences/pick_account_layouts.rb
107
+ - app/models/keystone_ui/preferences/pick_appearance.rb
88
108
  - app/models/keystone_ui/preferences/pick_navigation_order.rb
89
109
  - app/models/keystone_ui/preferences/pick_navigation_placement.rb
90
110
  - app/models/keystone_ui/preferences/refusal.rb
111
+ - app/views/keystone_ui/preferences/settings/_account_appearance.html.erb
91
112
  - app/views/keystone_ui/preferences/settings/_account_layouts.html.erb
113
+ - app/views/keystone_ui/preferences/settings/_appearance.html.erb
114
+ - app/views/keystone_ui/preferences/settings/_colour_choices.html.erb
115
+ - app/views/keystone_ui/preferences/settings/_look_choices.html.erb
92
116
  - app/views/keystone_ui/preferences/settings/_navigation_order.html.erb
93
117
  - app/views/keystone_ui/preferences/settings/_navigation_placement.html.erb
94
118
  - config/routes.rb
95
119
  - lib/generators/keystone_ui/preferences/install/install_generator.rb
96
120
  - lib/generators/keystone_ui/preferences/install/templates/create_keystone_ui_preferences_component_preferences.rb.erb
121
+ - lib/generators/keystone_ui/preferences/update/templates/copy_keystone_ui_colors_theme_preferences.rb.erb
97
122
  - lib/generators/keystone_ui/preferences/update/templates/remove_extra_owner_index_from_keystone_ui_preferences_component_preferences.rb.erb
98
123
  - lib/generators/keystone_ui/preferences/update/update_generator.rb
99
124
  - lib/keystone_ui-preferences.rb
100
125
  - lib/keystone_ui/preferences.rb
126
+ - lib/keystone_ui/preferences/colors_check.rb
101
127
  - lib/keystone_ui/preferences/configuration.rb
102
128
  - lib/keystone_ui/preferences/engine.rb
103
129
  - lib/keystone_ui/preferences/version.rb