avo 4.0.0.beta.40 → 4.0.0.beta.41

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/app/assets/builds/avo/application.css +74 -53
  4. data/app/assets/builds/avo/application.js +99 -99
  5. data/app/assets/builds/avo/application.js.map +4 -4
  6. data/app/assets/stylesheets/application.css +1 -1
  7. data/app/assets/stylesheets/css/components/breadcrumbs.css +4 -4
  8. data/app/assets/stylesheets/css/components/color_scheme_switcher.css +3 -3
  9. data/app/assets/stylesheets/css/fields/stars.css +3 -3
  10. data/app/assets/stylesheets/css/fields/tags.css +5 -0
  11. data/app/assets/stylesheets/css/layout.css +14 -5
  12. data/app/assets/stylesheets/css/variables.css +37 -0
  13. data/app/components/avo/field_wrapper_component.html.erb +1 -1
  14. data/app/components/avo/fields/boolean_field/edit_component.html.erb +2 -2
  15. data/app/components/avo/fields/common/boolean_check_component.rb +3 -3
  16. data/app/components/avo/fields/common/files/view_type/grid_item_component.html.erb +3 -3
  17. data/app/components/avo/fields/common/files/view_type/grid_item_component.rb +2 -2
  18. data/app/components/avo/fields/common/status_viewer_component.html.erb +1 -1
  19. data/app/components/avo/fields/file_field/edit_component.html.erb +1 -1
  20. data/app/components/avo/fields/files_field/edit_component.html.erb +1 -1
  21. data/app/components/avo/fields/preview_field/index_component.rb +1 -1
  22. data/app/components/avo/fields/tags_field/tag_component.html.erb +1 -1
  23. data/app/components/avo/paginator_component.html.erb +13 -9
  24. data/app/controllers/avo/attachments_controller.rb +20 -4
  25. data/app/javascript/application.js +2 -13
  26. data/app/javascript/js/controllers/appearance_controller.js +2 -2
  27. data/app/javascript/js/controllers/appearance_preview_controller.js +34 -0
  28. data/app/javascript/js/controllers.js +2 -0
  29. data/app/javascript/js/global_hotkeys.js +2 -2
  30. data/app/views/avo/partials/_color_theme_override.html.erb +13 -0
  31. data/app/views/avo/private/appearance.html.erb +20 -55
  32. data/bin/setup +91 -0
  33. data/lib/avo/version.rb +1 -1
  34. data/lib/generators/avo/templates/locales/avo.ar.yml +7 -0
  35. data/lib/generators/avo/templates/locales/avo.de.yml +3 -0
  36. data/lib/generators/avo/templates/locales/avo.en.yml +3 -0
  37. data/lib/generators/avo/templates/locales/avo.es.yml +3 -0
  38. data/lib/generators/avo/templates/locales/avo.fr.yml +3 -0
  39. data/lib/generators/avo/templates/locales/avo.it.yml +3 -0
  40. data/lib/generators/avo/templates/locales/avo.ja.yml +3 -0
  41. data/lib/generators/avo/templates/locales/avo.nb.yml +3 -0
  42. data/lib/generators/avo/templates/locales/avo.nl.yml +3 -0
  43. data/lib/generators/avo/templates/locales/avo.nn.yml +3 -0
  44. data/lib/generators/avo/templates/locales/avo.pl.yml +5 -0
  45. data/lib/generators/avo/templates/locales/avo.pt-BR.yml +3 -0
  46. data/lib/generators/avo/templates/locales/avo.pt.yml +3 -0
  47. data/lib/generators/avo/templates/locales/avo.ro.yml +4 -0
  48. data/lib/generators/avo/templates/locales/avo.ru.yml +5 -0
  49. data/lib/generators/avo/templates/locales/avo.tr.yml +3 -0
  50. data/lib/generators/avo/templates/locales/avo.ua.yml +5 -0
  51. data/lib/generators/avo/templates/locales/avo.zh-TW.yml +3 -0
  52. data/lib/generators/avo/templates/locales/avo.zh.yml +3 -0
  53. metadata +3 -2
  54. data/bin/init +0 -52
@@ -97,6 +97,7 @@
97
97
 
98
98
  @import "./css/components/ui/checkbox.css";
99
99
  @import "./css/components/tooltip.css";
100
+ @import "./css/fields/tags.css";
100
101
 
101
102
  /* variables.css uses explicit @layer theme { .dark } and @layer components
102
103
  { .neutral-theme-*, .accent-theme-* } blocks internally. Importing it OUTSIDE
@@ -125,7 +126,6 @@
125
126
  @import "./css/fields/progress.css";
126
127
  @import "./css/fields/key_value.css";
127
128
  @import "./css/fields/trix.css";
128
- @import "./css/fields/tags.css";
129
129
  @import "./css/fields/tiptap.css";
130
130
  @import "./css/fields/stars.css";
131
131
 
@@ -1,5 +1,5 @@
1
1
  .breadcrumbs {
2
- @apply sticky z-40 w-full bg-primary mb-4;
2
+ @apply sticky z-40 w-full bg-(--color-main-content-background) mb-4;
3
3
  /* top: calc(var(--top-navbar-height) + var(--spacing) * 4); */
4
4
  top: calc(var(--top-navbar-height) + var(--spacing) * 2);
5
5
  margin-top: --spacing(-2);
@@ -14,7 +14,7 @@
14
14
  right: 0;
15
15
  height: calc(0.5rem);
16
16
  transform: translateY(-100%);
17
- background: var(--color-primary);
17
+ background: var(--color-main-content-background);
18
18
  pointer-events: none;
19
19
  }
20
20
  }
@@ -32,8 +32,8 @@
32
32
  height: --spacing(4);
33
33
  background: linear-gradient(
34
34
  to bottom,
35
- var(--color-primary),
36
- color-mix(in oklab, var(--color-primary), transparent 100%)
35
+ var(--color-main-content-background),
36
+ color-mix(in oklab, var(--color-main-content-background), transparent 100%)
37
37
  );
38
38
  pointer-events: none;
39
39
  }
@@ -115,9 +115,9 @@
115
115
  @apply bg-primary text-content shadow-sm;
116
116
  }
117
117
 
118
- /* Shortcut badge buttons - active state mirrors body.hotkeys-hide-badges. */
119
- body:not(.hotkeys-hide-badges) .color-scheme-switcher__scheme-button[data-key-badges="show"],
120
- body.hotkeys-hide-badges .color-scheme-switcher__scheme-button[data-key-badges="hide"] {
118
+ /* Shortcut badge buttons - active state mirrors :root.hotkeys-hide-badges. */
119
+ :root:not(.hotkeys-hide-badges) .color-scheme-switcher__scheme-button[data-key-badges="show"],
120
+ :root.hotkeys-hide-badges .color-scheme-switcher__scheme-button[data-key-badges="hide"] {
121
121
  @apply bg-primary text-content shadow-sm;
122
122
  }
123
123
 
@@ -1,10 +1,10 @@
1
1
  [data-component="avo/fields/common/stars_component"] {
2
2
  .star {
3
- @apply h-5 w-5 text-gray-300 transition-colors;
3
+ @apply h-5 w-5 text-tertiary transition-colors;
4
4
  }
5
-
5
+
6
6
  .star.filled {
7
- @apply text-yellow-400;
7
+ @apply text-warning-foreground;
8
8
  }
9
9
 
10
10
  .rating-group {
@@ -8,6 +8,11 @@ tags.tagify {
8
8
  --tags-focus-border-color: var(--color-primary);
9
9
  --placeholder-color: var(--color-content-secondary);
10
10
  --placeholder-color-focus: var(--color-content-secondary);
11
+ --tag-text-color: var(--color-content);
12
+ --tag-text-color--edit: var(--color-content);
13
+ --tag-remove-btn-color: var(--color-content);
14
+ --tag-bg: var(--color-secondary);
15
+ --tag-hover: var(--color-tertiary);
11
16
 
12
17
  padding-block: var(--input-py);
13
18
  align-items: center;
@@ -1,9 +1,7 @@
1
1
  @theme {
2
2
  --top-navbar-height: 3rem;
3
3
 
4
- --navbar-bg: var(--color-avo-neutral-900);
5
- --navbar-notch-radius: 1rem;
6
- --navbar-notch-color: var(--navbar-bg);
4
+ --navbar-notch-color: var(--color-navbar-background);
7
5
 
8
6
  --border-color: var(--color-tertiary);
9
7
  }
@@ -66,7 +64,7 @@
66
64
  }
67
65
 
68
66
  .main-content {
69
- @apply w-(--content-width) flex flex-col bg-primary py-2 lg:py-4 px-2 lg:px-4 border-s border-(--border-color) ms-1 rounded-se-(--navbar-notch-radius) rounded-ss-(--navbar-notch-radius);
67
+ @apply w-(--content-width) flex flex-col bg-(--color-main-content-background) py-2 lg:py-4 px-2 lg:px-4 border-s border-(--color-main-content-border) ms-1 rounded-se-(--navbar-notch-radius) rounded-ss-(--navbar-notch-radius);
70
68
 
71
69
  transition: margin 0.1s ease-in-out, width 0.1s ease-in-out;
72
70
  min-height: calc(100dvh - var(--top-navbar-height) - var(--spacing));
@@ -101,7 +99,7 @@
101
99
  gap: 0.75rem;
102
100
  padding-inline: 0.5rem;
103
101
 
104
- background-color: var(--navbar-bg);
102
+ background-color: var(--color-navbar-background);
105
103
 
106
104
  > * {
107
105
  @apply pointer-events-auto;
@@ -127,6 +125,17 @@
127
125
  /* border: 1px solid red; */
128
126
  }
129
127
 
128
+ /* Hide the arches when --navbar-notch-enabled is false. A style query lets the
129
+ variable read like a boolean (true/false) instead of exposing raw CSS
130
+ display keywords. The pseudo-elements query their originating element
131
+ (.top-navbar), which inherits --navbar-notch-enabled from :root. */
132
+ @container style(--navbar-notch-enabled: false) {
133
+ &::before,
134
+ &::after {
135
+ display: none;
136
+ }
137
+ }
138
+
130
139
  &::before {
131
140
  left: 0;
132
141
  background: radial-gradient(
@@ -73,7 +73,44 @@
73
73
  --color-brand-accent: oklch(39.04% 0 89.88);
74
74
 
75
75
  /* Semantic variables */
76
+
77
+ /* Sidebar background. Dedicated knob so installs can recolor the sidebar
78
+ independently of the neutral palette. Defaults to the page background in
79
+ light mode and a subtly tinted surface in dark (see the .dark override). */
76
80
  --color-sidebar-background: var(--color-background);
81
+
82
+ /* Top navbar background. Dedicated knob so installs can recolor the navbar
83
+ without touching the neutral palette. Defaults to the page background so
84
+ the navbar blends with the canvas; resolves per-scheme automatically since
85
+ --color-background is itself redefined in .dark. Override it (here or in
86
+ `.dark`) to give the navbar its own color. */
87
+ --color-navbar-background: var(--color-avo-neutral-900);
88
+
89
+ /* Background of the main content panel. Dedicated knob so installs can recolor
90
+ the content surface independently of the primary surface token. Defaults to
91
+ --color-primary; resolves per-scheme since --color-primary is redefined in
92
+ .dark. */
93
+ --color-main-content-background: var(--color-primary);
94
+
95
+ /* Border between the sidebar and the main content panel. Dedicated knob so
96
+ installs can restyle just this seam without touching the shared
97
+ --border-color (which also draws the sidebar-status borders). Tracks
98
+ --border-color by default, so it resolves per-scheme automatically. */
99
+ --color-main-content-border: var(--border-color);
100
+ }
101
+
102
+ /* Navbar notch knobs live in a plain :root block, not in @theme:
103
+ --navbar-notch-enabled is read by a style query (Tailwind tree-shakes @theme
104
+ vars not referenced via var(), so it would be stripped), and we keep
105
+ --navbar-notch-radius alongside it so both notch knobs sit together. */
106
+ :root {
107
+ /* Set to `false` to hide the navbar's inverted corner arches (e.g. when the
108
+ navbar and content share a background). */
109
+ --navbar-notch-enabled: true;
110
+
111
+ /* Radius of the content panel's rounded top corners (and the matching navbar
112
+ arches that fill them). Set to `0` to flatten the corners entirely. */
113
+ --navbar-notch-radius: 1rem;
77
114
  }
78
115
 
79
116
  /* Dark mode defaults live in @layer theme — same layer as the @theme block
@@ -12,7 +12,7 @@
12
12
  <% else %>
13
13
  <%= @field.name %>
14
14
  <% end %>
15
- <% if on_edit? && @field.is_required? %> <span class="text-red-600 ms-1">*</span> <% end %>
15
+ <% if on_edit? && @field.is_required? %> <span class="text-danger-content ms-1">*</span> <% end %>
16
16
  <% if label_help.present? %>
17
17
  <div class="field-wrapper__label-help"><%== label_help %></div>
18
18
  <% end %>
@@ -21,8 +21,8 @@
21
21
  ) %>
22
22
 
23
23
  <% if as_toggle? %>
24
- <div class="block w-10 h-6 rounded-full bg-gray-300 peer-checked:bg-green-500"></div>
25
- <div class="dot absolute start-1 top-1 bg-white w-4 h-4 rounded-full transition-transform duration-200 transform peer-checked:translate-x-4"></div>
24
+ <div class="block w-10 h-6 rounded-full bg-tertiary peer-checked:bg-success"></div>
25
+ <div class="dot absolute start-1 top-1 bg-primary w-4 h-4 rounded-full transition-transform duration-200 transform peer-checked:translate-x-4"></div>
26
26
  <% end %>
27
27
  <% end %>
28
28
  </div>
@@ -5,17 +5,17 @@ class Avo::Fields::Common::BooleanCheckComponent < Avo::BaseComponent
5
5
  true => {
6
6
  name: "checked",
7
7
  icon: "tabler/outline/circle-check",
8
- color: "text-green-600"
8
+ color: "text-success-content"
9
9
  },
10
10
  false => {
11
11
  name: "unchecked",
12
12
  icon: "tabler/outline/circle-x",
13
- color: "text-red-600"
13
+ color: "text-danger-content"
14
14
  },
15
15
  nil => {
16
16
  name: "indeterminate",
17
17
  icon: "tabler/outline/circle-minus",
18
- color: "text-gray-400"
18
+ color: "text-content-secondary"
19
19
  }
20
20
  }.freeze
21
21
 
@@ -1,7 +1,7 @@
1
1
  <div id="<%= dom_id file %>" class="relative min-h-full max-w-full flex-1 flex flex-col justify-between space-y-2">
2
2
  <div class="flex flex-col h-full">
3
3
  <% if file.representable? && is_image? %>
4
- <%= image_tag helpers.main_app.url_for(file), class: "rounded-lg max-w-full self-start #{@extra_classes}", loading: :lazy, width: file.metadata["width"], height: file.metadata["height"] %>
4
+ <%= image_tag helpers.main_app.url_for(file), class: "rounded-lg max-w-full h-auto self-start object-cover #{@extra_classes}", loading: :lazy, width: file.metadata["width"], height: file.metadata["height"] %>
5
5
  <% elsif is_audio? %>
6
6
  <%= audio_tag(helpers.main_app.url_for(file), controls: true, preload: false, class: 'w-full') %>
7
7
  <% elsif is_video? %>
@@ -9,12 +9,12 @@
9
9
  <% else %>
10
10
  <%= content_tag file.representable? ? :a : :div, **document_arguments do %>
11
11
  <div class="flex flex-col justify-center items-center w-full">
12
- <%= helpers.svg "tabler/outline/file-text", class: 'h-10 text-gray-600 mb-2' %>
12
+ <%= helpers.svg "tabler/outline/file-text", class: 'h-10 text-content-secondary mb-2' %>
13
13
  </div>
14
14
  <% end %>
15
15
  <% end %>
16
16
  <% if @field.display_filename %>
17
- <span class="text-gray-500 mt-1 text-sm truncate" title="<%= file.filename %>"><%= file.filename %></span>
17
+ <span class="text-content-secondary mt-1 text-sm truncate" title="<%= file.filename %>"><%= file.filename %></span>
18
18
  <% end %>
19
19
  </div>
20
20
  <div class="flex space-x-2 rtl:space-x-reverse">
@@ -51,9 +51,9 @@ class Avo::Fields::Common::Files::ViewType::GridItemComponent < Avo::BaseCompone
51
51
  def document_arguments
52
52
  args = {
53
53
  class: class_names(
54
- "relative flex flex-col justify-evenly items-center px-2 rounded-lg border bg-white border-gray-500 min-h-24",
54
+ "relative flex flex-col justify-evenly items-center px-2 rounded-lg border bg-primary border-tertiary min-h-24",
55
55
  {
56
- "hover:bg-gray-100 transition": file.representable?
56
+ "hover:bg-secondary transition": file.representable?
57
57
  }
58
58
  )
59
59
  }
@@ -1,4 +1,4 @@
1
- <div class=" <%= class_names("flex shrink-0 items-center", "text-red-600": @status == 'failed', "text-green-600": @status == 'success', "text-gray-600": @status == 'neutral') %>">
1
+ <div class=" <%= class_names("flex shrink-0 items-center", "text-danger-content": @status == 'failed', "text-success-content": @status == 'success', "text-content-secondary": @status == 'neutral') %>">
2
2
  <% if @status == 'success' %>
3
3
  <%= helpers.svg 'tabler/filled/circle-check', class: 'h-4' %>
4
4
  <% elsif @status == 'failed' %>
@@ -31,7 +31,7 @@
31
31
  %>
32
32
  <% end %>
33
33
  <%= content_tag :button,
34
- class: "self-center hidden font-semibold text-xs text-red-600 p-1 cursor-pointer mt-2",
34
+ class: "self-center hidden font-semibold text-xs text-danger-content p-1 cursor-pointer mt-2",
35
35
  id: :reset,
36
36
  type: :button,
37
37
  data: {
@@ -26,7 +26,7 @@
26
26
  %>
27
27
  <% end %>
28
28
  <%= content_tag :button,
29
- class: "self-center hidden font-semibold text-xs text-red-600 p-1 cursor-pointer",
29
+ class: "self-center hidden font-semibold text-xs text-danger-content p-1 cursor-pointer",
30
30
  id: :reset,
31
31
  type: :button,
32
32
  data: {
@@ -5,7 +5,7 @@ class Avo::Fields::PreviewField::IndexComponent < Avo::Fields::IndexComponent
5
5
  link_to resource_view_path, title: t("avo.view_item", item: @resource.name).humanize do
6
6
  helpers.svg(
7
7
  "tabler/outline/zoom-scan",
8
- class: "block h-6 text-gray-600",
8
+ class: "block h-6 text-content-secondary",
9
9
  data: {
10
10
  controller: "preview",
11
11
  preview_url_value: helpers.preview_resource_path(resource: @resource, turbo_frame: :preview_modal),
@@ -1,4 +1,4 @@
1
- <%= tag.div class: "flex px-2 py-0.5 rounded-sm bg-gray-100 text-sm text-gray-800 font-normal shrink-0",
1
+ <%= tag.div class: "flex px-2 py-0.5 rounded-sm bg-secondary text-sm text-content font-normal shrink-0",
2
2
  data: {
3
3
  target: "tag-component",
4
4
  tippy: (@title.present? ? "tooltip" : nil),
@@ -32,16 +32,20 @@
32
32
  <div class="pagination__controls">
33
33
  <% if @resource.pagination_type.default? %>
34
34
  <div class="pagination__info">
35
- <span class="pagination__info-number">
36
- <%= "#{formatted_number(@pagy.from)}-#{formatted_number(@pagy.to)}" %>
37
- </span>
38
- &nbsp; of &nbsp;
39
- <% if @pagy.count >= 10_000 %>
40
- <span title="<%= formatted_count %>" data-tippy="tooltip">
41
- <%= number_to_social(@pagy.count, start_at: 10_000) %>
42
- </span>
35
+ <% if @pagy.pages <= 1 %>
36
+ <span><%= formatted_count %> <%= t("avo.record", count: @pagy.count) %></span>
43
37
  <% else %>
44
- <span><%= formatted_count %></span>
38
+ <span class="pagination__info-number">
39
+ <%= formatted_number(@pagy.from) %>-<%= formatted_number(@pagy.to) %>
40
+ </span>
41
+ &nbsp; of &nbsp;
42
+ <% if @pagy.count >= 10_000 %>
43
+ <span title="<%= formatted_count %>" data-tippy="tooltip">
44
+ <%= number_to_social(@pagy.count, start_at: 10_000) %>
45
+ </span>
46
+ <% else %>
47
+ <span><%= formatted_count %></span>
48
+ <% end %>
45
49
  <% end %>
46
50
  </div>
47
51
  <% end %>
@@ -7,14 +7,18 @@ module Avo
7
7
  before_action :set_record, only: [:destroy, :create]
8
8
 
9
9
  def create
10
- blob = ActiveStorage::Blob.create_and_upload! io: params[:file].to_io, filename: params[:filename]
11
10
  association_name = BaseResource.valid_attachment_name(@record, params[:attachment_key])
12
11
 
13
- # If association name is present attach the blob to it
14
12
  if association_name
13
+ return render_upload_unauthorized unless authorized_to_upload(association_name)
14
+
15
+ blob = ActiveStorage::Blob.create_and_upload! io: params[:file].to_io, filename: params[:filename]
15
16
  @record.send(association_name).attach blob
16
- # If key is present use the blob from the key else raise error
17
- elsif params[:key].blank?
17
+ elsif params[:key].present?
18
+ return render_upload_unauthorized unless authorized_to_trix_upload?
19
+
20
+ blob = ActiveStorage::Blob.create_and_upload! io: params[:file].to_io, filename: params[:filename]
21
+ else
18
22
  raise ActionController::BadRequest.new("Could not find the attachment association for #{params[:attachment_key]} (check the `attachment_key` for this Trix field)")
19
23
  end
20
24
 
@@ -63,5 +67,17 @@ module Avo
63
67
  def authorized_to(action)
64
68
  @resource.authorization.authorize_action("#{action}_#{params[:attachment_name]}?", record: @record, raise_exception: false)
65
69
  end
70
+
71
+ def authorized_to_upload(attachment_name)
72
+ @resource.authorization.authorize_action("upload_#{attachment_name}?", record: @record, raise_exception: false)
73
+ end
74
+
75
+ def authorized_to_trix_upload?
76
+ @resource.authorization.authorize_action("update?", record: @record, raise_exception: false)
77
+ end
78
+
79
+ def render_upload_unauthorized
80
+ render json: {error: "Not authorized"}, status: :forbidden
81
+ end
66
82
  end
67
83
  end
@@ -80,19 +80,8 @@ document.addEventListener('turbo:frame-render', (e) => {
80
80
  })
81
81
 
82
82
  document.addEventListener('turbo:load', () => {
83
- // Restore badge visibility preference from localStorage before installing hotkeys
84
- if (window.Avo?.configuration?.hotkeys?.showKeyBadges !== false) {
85
- try {
86
- if (localStorage.getItem('avo:hotkeys:hide_badges') === '1') {
87
- document.body.classList.add('hotkeys-hide-badges')
88
- } else {
89
- document.body.classList.remove('hotkeys-hide-badges')
90
- }
91
- } catch (e) {
92
- // localStorage unavailable
93
- }
94
- }
95
-
83
+ // Badge visibility preference is restored pre-paint on <html> in
84
+ // _color_theme_override.html.erb; the class persists across Turbo navigations.
96
85
  if (window.Avo?.configuration?.hotkeys?.enabled !== false) installHotkeys()
97
86
  initTippy()
98
87
 
@@ -327,14 +327,14 @@ export default class extends Controller {
327
327
  }
328
328
 
329
329
  // Mirrors the Shift+K hotkey in global_hotkeys.js. CSS handles the active
330
- // state via [data-key-badges] selectors against `body.hotkeys-hide-badges`.
330
+ // state via [data-key-badges] selectors against `:root.hotkeys-hide-badges`.
331
331
  setKeyBadges(event) {
332
332
  event.preventDefault()
333
333
  const { keyBadges } = event.currentTarget.dataset
334
334
  if (keyBadges !== 'show' && keyBadges !== 'hide') return
335
335
 
336
336
  const hide = keyBadges === 'hide'
337
- document.body.classList.toggle('hotkeys-hide-badges', hide)
337
+ document.documentElement.classList.toggle('hotkeys-hide-badges', hide)
338
338
  try {
339
339
  if (hide) {
340
340
  localStorage.setItem('avo:hotkeys:hide_badges', '1')
@@ -0,0 +1,34 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+
3
+ // Powers the internal color-token reference page (private#appearance): tab
4
+ // switching between token panels and click-to-copy on swatches, with a shared
5
+ // "Copied!" toast. Replaces the page's former inline <script> + onclick handlers
6
+ // so the page stays CSP-safe (no inline script execution).
7
+ export default class extends Controller {
8
+ static targets = ['indicator']
9
+
10
+ switchTab(event) {
11
+ event.preventDefault()
12
+ const button = event.currentTarget
13
+ const group = button.closest('.tabs')
14
+ const container = group.parentElement
15
+
16
+ container.querySelectorAll('.tab-content').forEach((tab) => tab.classList.remove('active'))
17
+ group.querySelectorAll('.tab-button').forEach((btn) => btn.classList.remove('active'))
18
+
19
+ document.getElementById(button.dataset.tab)?.classList.add('active')
20
+ button.classList.add('active')
21
+ }
22
+
23
+ copy(event) {
24
+ const { copyText } = event.currentTarget.dataset
25
+ if (!copyText) return
26
+
27
+ navigator.clipboard.writeText(copyText).then(() => {
28
+ if (!this.hasIndicatorTarget) return
29
+
30
+ this.indicatorTarget.classList.add('show')
31
+ setTimeout(() => this.indicatorTarget.classList.remove('show'), 2000)
32
+ })
33
+ }
34
+ }
@@ -11,6 +11,7 @@ import CheckboxListFieldController from './controllers/fields/checkbox_list_fiel
11
11
  import ClearInputController from './controllers/fields/clear_input_controller'
12
12
  import CodeFieldController from './controllers/fields/code_field_controller'
13
13
  import AppearanceController from './controllers/appearance_controller'
14
+ import AppearancePreviewController from './controllers/appearance_preview_controller'
14
15
  import ConfirmDialogController from './controllers/confirm_dialog_controller'
15
16
  import CopyToClipboardController from './controllers/copy_to_clipboard_controller'
16
17
  import DashboardCardController from './controllers/dashboard_card_controller'
@@ -77,6 +78,7 @@ application.register('boolean-filter', BooleanFilterController)
77
78
  application.register('card-filters', CardFiltersController)
78
79
  application.register('clear-input', ClearInputController)
79
80
  application.register('appearance', AppearanceController)
81
+ application.register('appearance-preview', AppearancePreviewController)
80
82
  application.register('copy-to-clipboard', CopyToClipboardController)
81
83
  application.register('dashboard-card', DashboardCardController)
82
84
  application.register('date-time-filter', DateTimeFilterController)
@@ -97,8 +97,8 @@ const DIRECT_HOTKEYS = [
97
97
  match: (e) => e.shiftKey && e.key === 'K'
98
98
  && window.Avo?.configuration?.hotkeys?.showKeyBadges !== false,
99
99
  handle: () => {
100
- document.body.classList.toggle('hotkeys-hide-badges')
101
- const hidden = document.body.classList.contains('hotkeys-hide-badges')
100
+ document.documentElement.classList.toggle('hotkeys-hide-badges')
101
+ const hidden = document.documentElement.classList.contains('hotkeys-hide-badges')
102
102
  try {
103
103
  if (hidden) {
104
104
  localStorage.setItem('avo:hotkeys:hide_badges', '1')
@@ -6,6 +6,19 @@
6
6
  var root = document.documentElement;
7
7
  var appearance = window.Avo && window.Avo.configuration && window.Avo.configuration.appearance || {};
8
8
 
9
+ // Restore the kbd-badge visibility preference (Shift+K) before paint so a
10
+ // hidden set of badges never flashes in. It's a per-client preference, so it
11
+ // lives in localStorage rather than a cookie; the class sits on <html> next
12
+ // to the theme classes, which also lets it persist across Turbo navigations.
13
+ var hotkeys = window.Avo && window.Avo.configuration && window.Avo.configuration.hotkeys || {};
14
+ if (hotkeys.showKeyBadges !== false) {
15
+ try {
16
+ if (localStorage.getItem('avo:hotkeys:hide_badges') === '1') {
17
+ root.classList.add('hotkeys-hide-badges');
18
+ }
19
+ } catch (e) {}
20
+ }
21
+
9
22
  if (appearance.persistence !== 'database') {
10
23
  function getCookie(name) {
11
24
  var value = '; ' + document.cookie;