trek 2.1.0 → 2.2.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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/Gemfile.lock +1 -1
  4. data/app/assets/icons/trek/audit.svg +1 -0
  5. data/app/assets/icons/trek/print.svg +1 -0
  6. data/app/components/trek/dashboard/analytics_component/analytics_component.html.slim +2 -0
  7. data/app/components/trek/dashboard/analytics_component.rb +1 -0
  8. data/app/components/trek/dashboard/chart_component/chart_component.css +192 -0
  9. data/app/components/trek/dashboard/chart_component/chart_component.html.slim +6 -0
  10. data/app/components/trek/dashboard/chart_component/chart_component.js +1 -0
  11. data/app/components/trek/dashboard/chart_component.rb +27 -0
  12. data/app/components/trek/dashboard/tile_component/tile_component.css +40 -0
  13. data/app/components/trek/dashboard/tile_component/tile_component.html.slim +14 -4
  14. data/app/components/trek/dashboard/tile_component.rb +6 -2
  15. data/app/components/trek/form/actions_component/actions_component.css +6 -7
  16. data/app/components/trek/form/content_editor_component/content_editor_component.html.slim +10 -12
  17. data/app/components/trek/form/content_editor_component/content_editor_component.js +4 -0
  18. data/app/components/trek/layout_component/layout_component.css +20 -0
  19. data/app/components/trek/layout_component/layout_component.html.slim +16 -0
  20. data/app/components/trek/layout_component/layout_component.js +35 -6
  21. data/app/formatters/prosemirror_to_html/global_id_to_links_formatter.rb +3 -1
  22. data/app/models/prosemirror.rb +5 -2
  23. data/app/views/trek/panels/images/_form.html.slim +1 -1
  24. data/app/views/trek/panels/images/create.turbo_stream.slim +3 -3
  25. data/app/views/trek/panels/images/new.html.slim +1 -1
  26. data/app/views/trek/panels/links/_form.html.slim +1 -1
  27. data/app/views/trek/panels/links/create.turbo_stream.slim +3 -3
  28. data/app/views/trek/panels/links/new.html.slim +1 -1
  29. data/app/views/trek/panels/prompts/_form.html.slim +1 -1
  30. data/app/views/trek/panels/prompts/create.turbo_stream.slim +3 -3
  31. data/app/views/trek/panels/prompts/new.html.slim +1 -1
  32. data/lib/trek/version.rb +1 -1
  33. data/package.json +1 -1
  34. metadata +7 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5a2b7ab45c51c036cab94f7ab2f6119b84cfccf501d1133cd4f390e320d40aa6
4
- data.tar.gz: '0803283b1a6283983d174c9797d5ea753f97ffc1ce458e0285ab2ac681257ecf'
3
+ metadata.gz: 76621e65ede62feb9ed4fd8aa029352bfbdda31c814a7e9679330597a631b2f6
4
+ data.tar.gz: a28bce934474a0d57069466281121658c75115eb9b9b1a3056eab9c6ee03ca5f
5
5
  SHA512:
6
- metadata.gz: 10aea34299dba23456b3f687c6e450996d5473e8d34d9a12200373c2e3f394025e6e5fc2aab4d2dfe00628133113171354780ab3123c6628dc5480e75ed87987
7
- data.tar.gz: 966a6004dc2aaa1b7ff21c4db7c87d00e3eb4054ea3dbaec780c62823b0d297f8736be2d93216dc438820375aa3487063882325fd55cb8f612ec831a9179c830
6
+ metadata.gz: 5653df741bf84cedce799a58c4f9d8c13d8a13226793f05d1f158b4971b0b08b1806463c82fa818526c8a0454d5b489f65cae207373c581bae074e1e3ae037bd
7
+ data.tar.gz: aad5fb5a6e0e9010c5b611ef2ba67e7ddf112d2b983c85684ce698d32083d1da82ac24469404f3377a131d3ef1be56ec369baa81bd9cd7e71b4560a916800e62
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [2.2.0] - 2026-08-28
4
+
5
+ ### Added
6
+
7
+ - `Trek::Dashboard::ChartComponent`, a dashboard card wrapping [Chartkick](https://chartkick.com) — pass `title`, `data`, a `type` (`:line_chart`, `:bar_chart`, …), optional `info`/`options`, and a `color` (default `:slate`); it renders `public_send(type, data, **options)` inside a coloured card matching the tiles. Chartkick is an **optional** dependency: the component only includes `Chartkick::Helper` when it is loaded, so Trek keeps working without it. `Trek::Dashboard::AnalyticsComponent` gained a `renders_many :charts` slot (alongside the existing `section`) so charts can be dropped straight into the dashboard with `a.with_chart(...)`. **Upgrade note:** to use `ChartComponent`, add `gem "chartkick"` to your app and `import "chartkick/chart.js"` in your admin JS bundle (Trek does not pull Chartkick in for you).
8
+ - `Trek::Dashboard::TileComponent` gained a `title` option (a large heading above the text, coloured per the tile's `color`) and a `wide` option (default `false`) that spans two tile columns. Its `href` is now optional: without one the tile renders as a plain `div` instead of a link, for read-only stat tiles.
9
+ - The layout gains a **stacked panel**: a second panel frame (`trek--layout-stacked-panel`) that opens on top of the primary one. The content editor's link/image/prompt inserters now open there, so an editor hosted _inside_ a panel (e.g. a Sectionable payload edited in a side panel) no longer loses its unsaved content when inserting a link — the primary panel stays put underneath and the inserted element lands back in the live editor. Closing the primary panel also closes the stacked one; _Annuler_ in the stacked panel closes only that one. The stacked panel is 1rem narrower than the primary panel (only while it stacks on an open one). **Upgrade note:** host apps that override the panel views (`app/views/<engine>/panels/{links,images,prompts}/*`) must retarget them to the `trek--layout-stacked-panel` frame and the `stackedPanelCloser` target, as Trek's own views now do.
10
+
11
+ ### Fixed
12
+
13
+ - Internal links inserted through the content editor (stored as `gid://` URLs) rendered as literal `gid://…` hrefs — or were dropped by the sanitizer — in formatted content: `Prosemirror#format_content` now resolves GlobalIDs to page paths before rendering, and the editor's Link extension accepts the `gid` protocol so reopening such a link shows the linked page instead of an empty field.
14
+ - `Trek::Form::ActionsComponent`: the sticky footer styling (sticky positioning, `--accent-1` background, `4rem` vertical padding) now only applies to the `default` aspect. It was set on the root rule, so the `seamless` aspect — used by `Trek::DialogComponent` — was also sticky and padded inside dialogs.
15
+
3
16
  ## [2.1.0] - 2026-08-25
4
17
 
5
18
  ### Added
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- trek (2.1.0)
4
+ trek (2.2.0)
5
5
  action_policy (~> 0.6)
6
6
  actioncable
7
7
  acts_as_list (~> 1.1)
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 18a2 2 0 0 0-4 0"/><path d="m19 11-2.11-6.657a2 2 0 0 0-2.752-1.148l-1.276.61A2 2 0 0 1 12 4H8.5a2 2 0 0 0-1.925 1.456L5 11"/><path d="M2 11h20"/><circle cx="17" cy="18" r="3"/><circle cx="7" cy="18" r="3"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><path d="M6 9V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6"/><rect x="6" y="14" width="12" height="8" rx="1"/></svg>
@@ -1,2 +1,4 @@
1
1
  div(class=root_class)
2
2
  div(class=class_for("info")) = section
3
+ - charts.each do |chart|
4
+ = chart
@@ -3,5 +3,6 @@
3
3
  module Trek
4
4
  class Dashboard::AnalyticsComponent < Trek::Component
5
5
  renders_one :section, Trek::Dashboard::SectionComponent
6
+ renders_many :charts, Trek::Dashboard::ChartComponent
6
7
  end
7
8
  end
@@ -0,0 +1,192 @@
1
+ .root {
2
+ border: 0.1rem solid;
3
+ border-radius: 0.8rem;
4
+ font-weight: var(--semibold);
5
+ margin-bottom: 2rem;
6
+ max-width: 83.7rem;
7
+ padding: 2rem;
8
+ transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out,
9
+ color 0.2s ease-in-out;
10
+ }
11
+
12
+ .root:last-child {
13
+ margin-bottom: 0;
14
+ }
15
+
16
+ .title {
17
+ color: var(--mint-12);
18
+ font-size: 1.6rem;
19
+ font-weight: var(--semibold);
20
+ margin: 0 0 0.5rem;
21
+ }
22
+
23
+ .data {
24
+ margin-bottom: 2rem;
25
+ }
26
+
27
+ .info {
28
+ color: var(--slate-11);
29
+ font-size: 1.4rem;
30
+ font-weight: var(--medium);
31
+ padding-bottom: 1.6rem;
32
+ }
33
+
34
+ .root.color-amber {
35
+ --icon-color: var(--amber-11);
36
+
37
+ background-color: var(--amber-2);
38
+ border-color: var(--amber-5);
39
+ color: var(--amber-12);
40
+ }
41
+
42
+ .root.color-amber:hover {
43
+ --icon-color: var(--amber-12);
44
+
45
+ background-color: var(--amber-3);
46
+ border-color: var(--amber-6);
47
+ }
48
+
49
+ .root.color-amber:active {
50
+ background-color: var(--amber-5);
51
+ border-color: var(--amber-7);
52
+ }
53
+
54
+ .root.color-lime {
55
+ --icon-color: var(--lime-11);
56
+
57
+ background-color: var(--lime-2);
58
+ border-color: var(--lime-5);
59
+ color: var(--lime-12);
60
+ }
61
+
62
+ .root.color-lime:hover {
63
+ --icon-color: var(--lime-12);
64
+
65
+ background-color: var(--lime-3);
66
+ border-color: var(--lime-6);
67
+ }
68
+
69
+ .root.color-lime:active {
70
+ background-color: var(--lime-5);
71
+ border-color: var(--lime-7);
72
+ }
73
+
74
+ .root.color-mint {
75
+ --icon-color: var(--mint-11);
76
+
77
+ background-color: var(--mint-2);
78
+ border-color: var(--mint-5);
79
+ color: var(--mint-12);
80
+ }
81
+
82
+ .root.color-mint:hover {
83
+ --icon-color: var(--mint-12);
84
+
85
+ background-color: var(--mint-3);
86
+ border-color: var(--mint-6);
87
+ }
88
+
89
+ .root.color-mint:active {
90
+ background-color: var(--mint-5);
91
+ border-color: var(--mint-7);
92
+ }
93
+
94
+ .root.color-purple {
95
+ --icon-color: var(--purple-11);
96
+
97
+ background-color: var(--purple-2);
98
+ border-color: var(--purple-5);
99
+ color: var(--purple-12);
100
+ }
101
+
102
+ .root.color-purple:hover {
103
+ --icon-color: var(--purple-12);
104
+
105
+ background-color: var(--purple-3);
106
+ border-color: var(--purple-6);
107
+ }
108
+
109
+ .root.color-purple:active {
110
+ background-color: var(--purple-5);
111
+ border-color: var(--purple-7);
112
+ }
113
+
114
+ .root.color-sky {
115
+ --icon-color: var(--sky-11);
116
+
117
+ background-color: var(--sky-2);
118
+ border-color: var(--sky-5);
119
+ color: var(--sky-12);
120
+ }
121
+
122
+ .root.color-sky:hover {
123
+ --icon-color: var(--sky-12);
124
+
125
+ background-color: var(--sky-3);
126
+ border-color: var(--sky-6);
127
+ }
128
+
129
+ .root.color-sky:active {
130
+ background-color: var(--sky-5);
131
+ border-color: var(--sky-7);
132
+ }
133
+
134
+ .root.color-slate {
135
+ --icon-color: var(--slate-11);
136
+
137
+ background-color: var(--slate-2);
138
+ border-color: var(--slate-5);
139
+ color: var(--slate-12);
140
+ }
141
+
142
+ .root.color-slate:hover {
143
+ --icon-color: var(--slate-12);
144
+
145
+ background-color: var(--slate-3);
146
+ border-color: var(--slate-6);
147
+ }
148
+
149
+ .root.color-slate:active {
150
+ background-color: var(--slate-5);
151
+ border-color: var(--slate-7);
152
+ }
153
+
154
+ .root.color-teal {
155
+ --icon-color: var(--teal-11);
156
+
157
+ background-color: var(--teal-2);
158
+ border-color: var(--teal-5);
159
+ color: var(--teal-12);
160
+ }
161
+
162
+ .root.color-teal:hover {
163
+ --icon-color: var(--teal-12);
164
+
165
+ background-color: var(--teal-3);
166
+ border-color: var(--teal-6);
167
+ }
168
+
169
+ .root.color-teal:active {
170
+ background-color: var(--teal-5);
171
+ border-color: var(--teal-7);
172
+ }
173
+
174
+ .root.color-violet {
175
+ --icon-color: var(--violet-11);
176
+
177
+ background-color: var(--violet-2);
178
+ border-color: var(--violet-5);
179
+ color: var(--violet-12);
180
+ }
181
+
182
+ .root.color-violet:hover {
183
+ --icon-color: var(--violet-12);
184
+
185
+ background-color: var(--violet-3);
186
+ border-color: var(--violet-6);
187
+ }
188
+
189
+ .root.color-violet:active {
190
+ background-color: var(--violet-5);
191
+ border-color: var(--violet-7);
192
+ }
@@ -0,0 +1,6 @@
1
+ div(class=classes)
2
+ div(class=class_for("title")) = title
3
+ - if info.present?
4
+ span(class=class_for("info")) = info
5
+ div(class=class_for("data"))
6
+ = chart
@@ -0,0 +1 @@
1
+ import "./chart_component.css";
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trek
4
+ class Dashboard::ChartComponent < Trek::Component
5
+ # Chartkick is an optional dependency: host apps that use this component must
6
+ # add `gem "chartkick"` and import "chartkick/chart.js" in their JS bundle.
7
+ include Chartkick::Helper if defined?(Chartkick)
8
+
9
+ option :title
10
+ option :info, optional: true
11
+ option :data
12
+ option :type
13
+ option :options, optional: true, default: proc { {} }
14
+ option :color, default: proc { :slate }
15
+
16
+ def chart
17
+ public_send(type, data, **options)
18
+ end
19
+
20
+ def classes
21
+ class_names(
22
+ root_class,
23
+ class_for("color-#{color}")
24
+ )
25
+ end
26
+ end
27
+ end
@@ -20,6 +20,14 @@
20
20
  width: 20rem;
21
21
  }
22
22
 
23
+ .root.wide {
24
+ width: calc(40rem + 1.2rem);
25
+ }
26
+
27
+ .title {
28
+ font-size: 3.6rem;
29
+ }
30
+
23
31
  .root:hover .icon {
24
32
  transform: scale(1.125);
25
33
  }
@@ -58,6 +66,10 @@
58
66
  border-color: var(--amber-7);
59
67
  }
60
68
 
69
+ .root.color-amber .title {
70
+ color: var(--amber-11);
71
+ }
72
+
61
73
  .root.color-lime {
62
74
  --icon-color: var(--lime-11);
63
75
 
@@ -78,6 +90,10 @@
78
90
  border-color: var(--lime-7);
79
91
  }
80
92
 
93
+ .root.color-lime .title {
94
+ color: var(--lime-11);
95
+ }
96
+
81
97
  .root.color-mint {
82
98
  --icon-color: var(--mint-11);
83
99
 
@@ -98,6 +114,10 @@
98
114
  border-color: var(--mint-7);
99
115
  }
100
116
 
117
+ .root.color-mint .title {
118
+ color: var(--mint-11);
119
+ }
120
+
101
121
  .root.color-purple {
102
122
  --icon-color: var(--purple-11);
103
123
 
@@ -118,6 +138,10 @@
118
138
  border-color: var(--purple-7);
119
139
  }
120
140
 
141
+ .root.color-purple .title {
142
+ color: var(--purple-11);
143
+ }
144
+
121
145
  .root.color-sky {
122
146
  --icon-color: var(--sky-11);
123
147
 
@@ -138,6 +162,10 @@
138
162
  border-color: var(--sky-7);
139
163
  }
140
164
 
165
+ .root.color-sky .title {
166
+ color: var(--sky-11);
167
+ }
168
+
141
169
  .root.color-slate {
142
170
  --icon-color: var(--slate-11);
143
171
 
@@ -158,6 +186,10 @@
158
186
  border-color: var(--slate-7);
159
187
  }
160
188
 
189
+ .root.color-slate .title {
190
+ color: var(--slate-11);
191
+ }
192
+
161
193
  .root.color-teal {
162
194
  --icon-color: var(--teal-11);
163
195
 
@@ -178,6 +210,10 @@
178
210
  border-color: var(--teal-7);
179
211
  }
180
212
 
213
+ .root.color-teal .title {
214
+ color: var(--teal-11);
215
+ }
216
+
181
217
  .root.color-violet {
182
218
  --icon-color: var(--violet-11);
183
219
 
@@ -197,3 +233,7 @@
197
233
  background-color: var(--violet-5);
198
234
  border-color: var(--violet-7);
199
235
  }
236
+
237
+ .root.color-violet .title {
238
+ color: var(--violet-11);
239
+ }
@@ -1,4 +1,14 @@
1
- = link_to href, class: classes, rel: rel, target: target
2
- - if icon
3
- = render Trek::IconComponent.new(icon, classnames: class_for("icon"))
4
- span(class=class_for("text")) = formatted_text
1
+ - if href.present?
2
+ = link_to href, class: classes, rel: rel, target: target
3
+ - if title
4
+ span(class=class_for("title")) = title
5
+ - if icon
6
+ = render Trek::IconComponent.new(icon, classnames: class_for("icon"))
7
+ span(class=class_for("text")) = formatted_text
8
+ - else
9
+ div(class=classes)
10
+ - if title
11
+ span(class=class_for("title")) = title
12
+ - if icon
13
+ = render Trek::IconComponent.new(icon, classnames: class_for("icon"))
14
+ span(class=class_for("text")) = formatted_text
@@ -3,12 +3,15 @@
3
3
  module Trek
4
4
  class Dashboard::TileComponent < Trek::Component
5
5
  option :color, default: proc { :slate }
6
- option :href
6
+ option :href, optional: true
7
7
  option :icon, optional: true
8
8
  option :rel, optional: true
9
9
  option :target, optional: true
10
10
  option :text
11
+ option :title, optional: true
11
12
  option :allowed_to, default: -> { true }
13
+ # Spans two tile columns (including the gap between them).
14
+ option :wide, default: proc { false }
12
15
 
13
16
  def render?
14
17
  allowed_to
@@ -17,7 +20,8 @@ module Trek
17
20
  def classes
18
21
  class_names(
19
22
  root_class,
20
- class_for("color-#{color}")
23
+ class_for("color-#{color}"),
24
+ class_for("wide") => wide
21
25
  )
22
26
  end
23
27
 
@@ -2,14 +2,8 @@
2
2
 
3
3
  .root {
4
4
  align-items: center;
5
- background-color: var(--accent-1);
6
- bottom: 0;
7
5
  display: flex;
8
6
  max-width: 120rem;
9
- padding: 4rem 0;
10
- position: sticky;
11
- width: 100%;
12
- z-index: 2;
13
7
  }
14
8
 
15
9
  .buttons {
@@ -36,9 +30,14 @@
36
30
 
37
31
  /* aspect */
38
32
  .root.aspect-default {
33
+ background-color: var(--accent-1);
39
34
  border-top: 1px solid var(--slate-6);
35
+ bottom: 0;
40
36
  margin-top: 3.2rem;
41
- padding-top: 3.2rem;
37
+ padding: 3.2rem 0 4rem;
38
+ position: sticky;
39
+ width: 100%;
40
+ z-index: 2;
42
41
  }
43
42
 
44
43
  /* .root.aspect-seamless {
@@ -29,16 +29,14 @@ div(
29
29
  / data-action=stimulus_action("toggleCode")
30
30
  / ) code
31
31
 
32
- / slim-lint:disable ControlStatementSpacing
32
+ / The panel opening is handled by the hidden button's
33
+ openStackedPanelWhenLoaded action — a direct openPanel here would
34
+ pop the (possibly empty) primary panel alongside the stacked one.
33
35
  button.link(
34
36
  class=class_for("button")
35
37
  type="button"
36
- data-action=[ \
37
- stimulus_action("toggleLink"),
38
- "trek--layout#openPanel",
39
- ].join(" ")
38
+ data-action=stimulus_action("toggleLink")
40
39
  ) = render Trek::IconComponent.new("trek/link", title: t(".link"))
41
- / slim-lint:enable ControlStatementSpacing
42
40
 
43
41
  = link_to "",
44
42
  new_link_path,
@@ -46,8 +44,8 @@ div(
46
44
  hidden: true,
47
45
  data: stimulus_target_hash("hiddenLinkButton").merge( \
48
46
  'base-path': new_link_path,
49
- action: "trek--layout#openPanelWhenLoaded",
50
- 'turbo-frame': "trek--layout-panel",
47
+ action: "trek--layout#openStackedPanelWhenLoaded",
48
+ 'turbo-frame': "trek--layout-stacked-panel",
51
49
  )
52
50
 
53
51
  button(
@@ -239,8 +237,8 @@ div(
239
237
  hidden: true,
240
238
  data: stimulus_target_hash("hiddenPromptsButton").merge( \
241
239
  'base-path': new_prompt_path,
242
- action: "trek--layout#openPanelWhenLoaded",
243
- 'turbo-frame': "trek--layout-panel",
240
+ action: "trek--layout#openStackedPanelWhenLoaded",
241
+ 'turbo-frame': "trek--layout-stacked-panel",
244
242
  )
245
243
  = link_to "",
246
244
  new_image_path,
@@ -248,6 +246,6 @@ div(
248
246
  hidden: true,
249
247
  data: stimulus_target_hash("hiddenImageButton").merge( \
250
248
  'base-path': new_image_path,
251
- action: "trek--layout#openPanelWhenLoaded",
252
- 'turbo-frame': "trek--layout-panel",
249
+ action: "trek--layout#openStackedPanelWhenLoaded",
250
+ 'turbo-frame': "trek--layout-stacked-panel",
253
251
  )
@@ -41,6 +41,10 @@ export class Controller extends BaseController {
41
41
  // }),
42
42
  Link.configure({
43
43
  openOnClick: false,
44
+ // Internal links are stored as GlobalIDs (resolved to URLs at render
45
+ // time); without this the gid:// scheme fails Link's URI validation
46
+ // and setLink silently does nothing.
47
+ protocols: ["gid"],
44
48
  HTMLAttributes: {
45
49
  "data-action": "trek--form--content-editor#selectLink",
46
50
  target: null,
@@ -34,6 +34,18 @@
34
34
  transform: translateX(0);
35
35
  }
36
36
 
37
+ /* Stacked panel: overlays the primary panel. */
38
+ .panel-stacked {
39
+ box-shadow: 0 0 16px var(--slate-a8);
40
+ z-index: 3;
41
+ }
42
+
43
+ /* Only when it actually stacks on an open primary panel, make it 1rem
44
+ narrower so a sliver of the panel underneath stays visible. */
45
+ .panel.open + .panel-stacked {
46
+ width: 41rem;
47
+ }
48
+
37
49
  .panel-close {
38
50
  --icon-size: 1.6rem;
39
51
 
@@ -65,4 +77,12 @@
65
77
  .content {
66
78
  margin: 6rem 3rem;
67
79
  }
80
+
81
+ .panel {
82
+ width: 100vw;
83
+ }
84
+
85
+ .panel.open + .panel-stacked {
86
+ width: calc(100vw - 1rem);
87
+ }
68
88
  }
@@ -18,3 +18,19 @@ div(
18
18
  data-action=stimulus_action("closePanel")
19
19
  )
20
20
  = render(Trek::IconComponent.new("trek/close"))
21
+ / Stacked panel: opens on top of the primary panel, so a panel-hosted form
22
+ (e.g. a content editor in a section panel) survives sub-panels like the
23
+ link/image/prompt inserters.
24
+ aside(
25
+ class=[class_for("panel"), class_for("panel-stacked")]
26
+ data=stimulus_target_hash("stackedPanel")
27
+ )
28
+ = turbo_frame_tag( \
29
+ "#{identifier}-stacked-panel",
30
+ data: stimulus_target_hash("stackedPanelFrame"),
31
+ )
32
+ button(
33
+ class=class_for("panel-close")
34
+ data-action=stimulus_action("closeStackedPanel")
35
+ )
36
+ = render(Trek::IconComponent.new("trek/close"))
@@ -5,7 +5,14 @@ import { useClickOutside } from "stimulus-use";
5
5
 
6
6
  export class Controller extends BaseController {
7
7
  static classes = ["open"];
8
- static targets = ["panel", "panelFrame", "panelCloser"];
8
+ static targets = [
9
+ "panel",
10
+ "panelFrame",
11
+ "panelCloser",
12
+ "stackedPanel",
13
+ "stackedPanelFrame",
14
+ "stackedPanelCloser",
15
+ ];
9
16
 
10
17
  connect() {
11
18
  useClickOutside(this, { element: this.panelTarget });
@@ -17,31 +24,53 @@ export class Controller extends BaseController {
17
24
  this.closePanel();
18
25
  }
19
26
 
27
+ stackedPanelCloserTargetConnected() {
28
+ this.closeStackedPanel();
29
+ }
30
+
20
31
  openPanelWhenLoaded() {
21
32
  this.panelFrameTarget.loaded.then(() => this.openPanel());
22
33
  }
23
34
 
35
+ openStackedPanelWhenLoaded() {
36
+ this.stackedPanelFrameTarget.loaded.then(() => this.openStackedPanel());
37
+ }
38
+
24
39
  openPanel() {
25
40
  this.panelTarget.classList.add(this.openClass);
26
41
  }
27
42
 
43
+ openStackedPanel() {
44
+ this.stackedPanelTarget.classList.add(this.openClass);
45
+ }
46
+
28
47
  closePanel() {
48
+ this.closeStackedPanel();
29
49
  this.panelTarget.classList.remove(this.openClass);
30
- this.emptyFrame();
50
+ this.emptyFrame(this.panelFrameTarget);
51
+ }
52
+
53
+ closeStackedPanel() {
54
+ this.stackedPanelTarget.classList.remove(this.openClass);
55
+ this.emptyFrame(this.stackedPanelFrameTarget);
31
56
  }
32
57
 
33
58
  clickOutside(e) {
34
59
  if (
35
60
  e.target.dataset.action &&
36
- e.target.dataset.action.includes("trek--layout#openPanel")
61
+ (e.target.dataset.action.includes("trek--layout#openPanel") ||
62
+ e.target.dataset.action.includes("trek--layout#openStackedPanel"))
37
63
  )
38
64
  return;
65
+ // Clicks inside the stacked panel are outside the primary panel element,
66
+ // but must not close anything.
67
+ if (this.stackedPanelTarget.contains(e.target)) return;
39
68
  this.closePanel();
40
69
  }
41
70
 
42
- emptyFrame() {
43
- while (this.panelFrameTarget.firstChild) {
44
- this.panelFrameTarget.removeChild(this.panelFrameTarget.firstChild);
71
+ emptyFrame(frame) {
72
+ while (frame.firstChild) {
73
+ frame.removeChild(frame.firstChild);
45
74
  }
46
75
  }
47
76
  }
@@ -2,7 +2,9 @@ module ProsemirrorToHtml
2
2
  class GlobalIdToLinksFormatter
3
3
  include Rails.application.routes.url_helpers
4
4
 
5
- GLOBAL_ID_REGEX = %r{(gid://[[:alnum:]]+/[\w:]+/[\w-]+)}
5
+ # The app segment allows hyphens and dots: GlobalID derives it from the
6
+ # Rails application name (e.g. "au-poste").
7
+ GLOBAL_ID_REGEX = %r{(gid://[\w.-]+/[\w:]+/[\w-]+)}
6
8
 
7
9
  def initialize(str)
8
10
  @str = str
@@ -10,9 +10,12 @@ class Prosemirror
10
10
  def format_content(parsed_content)
11
11
  return if parsed_content.blank?
12
12
 
13
- content = renderer.render(parsed_content)
13
+ # GlobalIDs must be resolved to URLs before rendering: the renderer's
14
+ # sanitizer strips href attributes with unknown protocols, so a gid://
15
+ # link would lose its href entirely.
16
+ content = ProsemirrorToHtml::GlobalIdToLinksFormatter.new(parsed_content.to_json).to_s
17
+ content = renderer.render(JSON.parse(content))
14
18
  content = ProsemirrorToHtml::RemoveEmptyParagraphsFormatter.new(content).to_s
15
- content = ProsemirrorToHtml::GlobalIdToLinksFormatter.new(content).to_s
16
19
  # rubocop:disable Rails/OutputSafety
17
20
  content.html_safe
18
21
  # rubocop:enable Rails/OutputSafety
@@ -24,7 +24,7 @@ h1.c-trek--layout-panel-title = t(".title")
24
24
  = render Trek::Form::ActionsComponent.new do |c|
25
25
  ruby:
26
26
  c.with_button(
27
- data: { action: "trek--layout#closePanel" },
27
+ data: { action: "trek--layout#closeStackedPanel" },
28
28
  text: t("admin.actions.cancel"),
29
29
  )
30
30
 
@@ -1,6 +1,6 @@
1
1
  - if @object.valid?
2
- = turbo_stream.append("trek--layout-panel")
3
- div(data-trek--layout-target="panelCloser")
2
+ = turbo_stream.append("trek--layout-stacked-panel")
3
+ div(data-trek--layout-target="stackedPanelCloser")
4
4
 
5
5
  = turbo_stream.append(@object.turbo_frame_id)
6
6
  div(
@@ -15,5 +15,5 @@
15
15
  text=@object.text
16
16
  )
17
17
  - else
18
- = turbo_stream.update("trek--layout-panel")
18
+ = turbo_stream.update("trek--layout-stacked-panel")
19
19
  = render "form"
@@ -1,2 +1,2 @@
1
- = turbo_frame_tag("trek--layout-panel")
1
+ = turbo_frame_tag("trek--layout-stacked-panel")
2
2
  = render "form"
@@ -28,7 +28,7 @@ h1.c-trek--layout-panel-title = t(".title")
28
28
  = render Trek::Form::ActionsComponent.new do |c|
29
29
  ruby:
30
30
  c.with_button(
31
- data: { action: "trek--layout#closePanel" },
31
+ data: { action: "trek--layout#closeStackedPanel" },
32
32
  text: t("admin.actions.cancel"),
33
33
  )
34
34
 
@@ -1,6 +1,6 @@
1
1
  - if @object.valid?
2
- = turbo_stream.append("trek--layout-panel")
3
- div(data-trek--layout-target="panelCloser")
2
+ = turbo_stream.append("trek--layout-stacked-panel")
3
+ div(data-trek--layout-target="stackedPanelCloser")
4
4
 
5
5
  = turbo_stream.append(@object.turbo_frame_id)
6
6
  div(
@@ -8,5 +8,5 @@
8
8
  data-params=@object.to_json
9
9
  )
10
10
  - else
11
- = turbo_stream.update("trek--layout-panel")
11
+ = turbo_stream.update("trek--layout-stacked-panel")
12
12
  = render "form"
@@ -1,2 +1,2 @@
1
- = turbo_frame_tag("trek--layout-panel")
1
+ = turbo_frame_tag("trek--layout-stacked-panel")
2
2
  = render "form"
@@ -26,7 +26,7 @@ h1.c-trek--layout-panel-title = t(".title")
26
26
  = render Trek::Form::ActionsComponent.new do |c|
27
27
  ruby:
28
28
  c.with_button(
29
- data: { action: "trek--layout#closePanel" },
29
+ data: { action: "trek--layout#closeStackedPanel" },
30
30
  text: t("admin.actions.cancel"),
31
31
  )
32
32
 
@@ -1,6 +1,6 @@
1
1
  - if @object.valid?
2
- = turbo_stream.append("trek--layout-panel")
3
- div(data-trek--layout-target="panelCloser")
2
+ = turbo_stream.append("trek--layout-stacked-panel")
3
+ div(data-trek--layout-target="stackedPanelCloser")
4
4
 
5
5
  = turbo_stream.append(@object.turbo_frame_id)
6
6
  div(
@@ -14,5 +14,5 @@
14
14
  title=@object.title
15
15
  )
16
16
  - else
17
- = turbo_stream.update("trek--layout-panel")
17
+ = turbo_stream.update("trek--layout-stacked-panel")
18
18
  = render "form"
@@ -1,2 +1,2 @@
1
- = turbo_frame_tag("trek--layout-panel")
1
+ = turbo_frame_tag("trek--layout-stacked-panel")
2
2
  = render "form"
data/lib/trek/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Trek
4
- VERSION = "2.1.0"
4
+ VERSION = "2.2.0"
5
5
  end
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trek",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "A modern content management & back-office system for Ruby on Rails",
5
5
  "homepage": "https://trek.etamin.studio",
6
6
  "license": "MIT",
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trek
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mohamed Bengrich
@@ -454,6 +454,7 @@ files:
454
454
  - app/assets/icons/trek/add.svg
455
455
  - app/assets/icons/trek/arrow-bar-left.svg
456
456
  - app/assets/icons/trek/arrow-bar-right.svg
457
+ - app/assets/icons/trek/audit.svg
457
458
  - app/assets/icons/trek/bold.svg
458
459
  - app/assets/icons/trek/check.svg
459
460
  - app/assets/icons/trek/chevron-double-left.svg
@@ -477,6 +478,7 @@ files:
477
478
  - app/assets/icons/trek/logout.svg
478
479
  - app/assets/icons/trek/paragraph.svg
479
480
  - app/assets/icons/trek/pic.svg
481
+ - app/assets/icons/trek/print.svg
480
482
  - app/assets/icons/trek/profile.svg
481
483
  - app/assets/icons/trek/quotation.svg
482
484
  - app/assets/icons/trek/rating.svg
@@ -510,6 +512,10 @@ files:
510
512
  - app/components/trek/dashboard/analytics_component/analytics_component.css
511
513
  - app/components/trek/dashboard/analytics_component/analytics_component.html.slim
512
514
  - app/components/trek/dashboard/analytics_component/analytics_component.js
515
+ - app/components/trek/dashboard/chart_component.rb
516
+ - app/components/trek/dashboard/chart_component/chart_component.css
517
+ - app/components/trek/dashboard/chart_component/chart_component.html.slim
518
+ - app/components/trek/dashboard/chart_component/chart_component.js
513
519
  - app/components/trek/dashboard/section_component.rb
514
520
  - app/components/trek/dashboard/section_component/section_component.css
515
521
  - app/components/trek/dashboard/section_component/section_component.html.slim