studio-engine 0.127.0 → 0.128.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +116 -0
- data/README.md +9 -3
- data/app/assets/javascripts/studio/page_transition_reveal.js +1 -1
- data/app/assets/tailwind/studio_engine/engine.css +519 -37
- data/app/controllers/studio/surveys_controller.rb +16 -3
- data/app/helpers/studio/loader_helper.rb +2 -2
- data/app/helpers/studio_icon_helper.rb +10 -0
- data/app/javascript/studio/agent_chat.js +14 -3
- data/app/javascript/studio/agent_chat_voice.js +42 -4
- data/app/javascript/studio/loader.js +130 -5
- data/app/javascript/studio/microphone_chip.js +28 -2
- data/app/javascript/studio/page_transitions.js +28 -19
- data/app/javascript/studio/survey.js +16 -3
- data/app/javascript/studio/theme_editor.js +4 -0
- data/app/javascript/studio/voice_recorder.js +18 -7
- data/app/models/studio/survey_response.rb +24 -17
- data/app/views/components/_geo_signpost.html.erb +1 -0
- data/app/views/components/_link_sidebar.html.erb +40 -12
- data/app/views/components/_sidebar_panel.html.erb +15 -5
- data/app/views/layouts/studio/_head.html.erb +18 -7
- data/app/views/layouts/studio/_smooth_load.html.erb +1 -0
- data/app/views/layouts/studio/component_preview.html.erb +1 -1
- data/app/views/studio/agent_chat/_turn.html.erb +3 -1
- data/app/views/studio/agent_chat/_voice.html.erb +28 -13
- data/app/views/studio/loader/_overlay.html.erb +4 -4
- data/app/views/style/_components.html.erb +1 -1
- data/app/views/style/_glass.html.erb +83 -0
- data/app/views/style/_host.html.erb +1 -1
- data/app/views/style/_modals.html.erb +11 -11
- data/app/views/style/_scrims.html.erb +1 -1
- data/app/views/style/_site_footer.html.erb +1 -1
- data/app/views/style/_tasks.html.erb +1 -1
- data/app/views/style/_theme.html.erb +56 -72
- data/app/views/style/_tricks.html.erb +13 -11
- data/app/views/style/_type_ladder.html.erb +33 -0
- data/app/views/style/index.html.erb +5 -1
- data/docs/SITE_FOOTER.md +1 -1
- data/lib/studio/agent_chat.rb +4 -2
- data/lib/studio/icons/README.md +12 -0
- data/lib/studio/icons/lucide/LICENSE +43 -0
- data/lib/studio/icons/lucide/activity.svg +15 -0
- data/lib/studio/icons/lucide/book-open.svg +16 -0
- data/lib/studio/icons/lucide/chart-column.svg +18 -0
- data/lib/studio/icons/lucide/diamond.svg +15 -0
- data/lib/studio/icons/lucide/door-stairwell.svg +19 -0
- data/lib/studio/icons/lucide/globe.svg +17 -0
- data/lib/studio/icons/lucide/hammer.svg +17 -0
- data/lib/studio/icons/lucide/inbox.svg +16 -0
- data/lib/studio/icons/lucide/key-round.svg +16 -0
- data/lib/studio/icons/lucide/layout-grid.svg +18 -0
- data/lib/studio/icons/lucide/log-out.svg +17 -0
- data/lib/studio/icons/lucide/message-circle.svg +15 -0
- data/lib/studio/icons/lucide/package.svg +18 -0
- data/lib/studio/icons/lucide/palette.svg +19 -0
- data/lib/studio/icons/lucide/plug.svg +18 -0
- data/lib/studio/icons/lucide/rocket.svg +18 -0
- data/lib/studio/icons/lucide/user.svg +16 -0
- data/lib/studio/icons.rb +52 -0
- data/lib/studio/loader.rb +7 -0
- data/lib/studio/sidebar_sections.rb +52 -4
- data/lib/studio/site_footer.rb +23 -2
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +54 -2
- metadata +24 -1
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lucide Icons and Contributors
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
The following Lucide icons are derived from the Feather project:
|
|
20
|
+
|
|
21
|
+
airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out
|
|
22
|
+
|
|
23
|
+
The MIT License (MIT) (for the icons listed above)
|
|
24
|
+
|
|
25
|
+
Copyright (c) 2013-present Cole Bemis
|
|
26
|
+
|
|
27
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
28
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
29
|
+
in the Software without restriction, including without limitation the rights
|
|
30
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
31
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
32
|
+
furnished to do so, subject to the following conditions:
|
|
33
|
+
|
|
34
|
+
The above copyright notice and this permission notice shall be included in all
|
|
35
|
+
copies or substantial portions of the Software.
|
|
36
|
+
|
|
37
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
38
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
39
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
40
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
41
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
42
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
43
|
+
SOFTWARE.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!-- @license lucide-static v1.68.0 - ISC -->
|
|
2
|
+
<svg
|
|
3
|
+
class="lucide lucide-activity"
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
width="24"
|
|
6
|
+
height="24"
|
|
7
|
+
viewBox="0 0 24 24"
|
|
8
|
+
fill="none"
|
|
9
|
+
stroke="currentColor"
|
|
10
|
+
stroke-width="2"
|
|
11
|
+
stroke-linecap="round"
|
|
12
|
+
stroke-linejoin="round"
|
|
13
|
+
>
|
|
14
|
+
<path d="M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2" />
|
|
15
|
+
</svg>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!-- @license lucide-static v1.68.0 - ISC -->
|
|
2
|
+
<svg
|
|
3
|
+
class="lucide lucide-book-open"
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
width="24"
|
|
6
|
+
height="24"
|
|
7
|
+
viewBox="0 0 24 24"
|
|
8
|
+
fill="none"
|
|
9
|
+
stroke="currentColor"
|
|
10
|
+
stroke-width="2"
|
|
11
|
+
stroke-linecap="round"
|
|
12
|
+
stroke-linejoin="round"
|
|
13
|
+
>
|
|
14
|
+
<path d="M12 5v16" />
|
|
15
|
+
<path d="M20.001 19A2 2 0 0022 17V5a2 2 0 00-1.999-2L16 3.002A5 5 0 0012 5a5 5 0 00-4-2H4a2 2 0 00-2 2v12a2 2 0 001.999 2H8a5 5 0 014 2 5 5 0 014-2z" />
|
|
16
|
+
</svg>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!-- @license lucide-static v1.68.0 - ISC -->
|
|
2
|
+
<svg
|
|
3
|
+
class="lucide lucide-chart-column"
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
width="24"
|
|
6
|
+
height="24"
|
|
7
|
+
viewBox="0 0 24 24"
|
|
8
|
+
fill="none"
|
|
9
|
+
stroke="currentColor"
|
|
10
|
+
stroke-width="2"
|
|
11
|
+
stroke-linecap="round"
|
|
12
|
+
stroke-linejoin="round"
|
|
13
|
+
>
|
|
14
|
+
<path d="M3 3v16a2 2 0 0 0 2 2h16" />
|
|
15
|
+
<path d="M18 17V9" />
|
|
16
|
+
<path d="M13 17V5" />
|
|
17
|
+
<path d="M8 17v-3" />
|
|
18
|
+
</svg>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!-- @license lucide-static v1.68.0 - ISC -->
|
|
2
|
+
<svg
|
|
3
|
+
class="lucide lucide-diamond"
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
width="24"
|
|
6
|
+
height="24"
|
|
7
|
+
viewBox="0 0 24 24"
|
|
8
|
+
fill="none"
|
|
9
|
+
stroke="currentColor"
|
|
10
|
+
stroke-width="2"
|
|
11
|
+
stroke-linecap="round"
|
|
12
|
+
stroke-linejoin="round"
|
|
13
|
+
>
|
|
14
|
+
<path d="M2.7 10.3a2.41 2.41 0 0 0 0 3.41l7.59 7.59a2.41 2.41 0 0 0 3.41 0l7.59-7.59a2.41 2.41 0 0 0 0-3.41l-7.59-7.59a2.41 2.41 0 0 0-3.41 0Z" />
|
|
15
|
+
</svg>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<!-- @license lucide-static v1.68.0 - ISC -->
|
|
2
|
+
<svg
|
|
3
|
+
class="lucide lucide-door-stairwell"
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
width="24"
|
|
6
|
+
height="24"
|
|
7
|
+
viewBox="0 0 24 24"
|
|
8
|
+
fill="none"
|
|
9
|
+
stroke="currentColor"
|
|
10
|
+
stroke-width="2"
|
|
11
|
+
stroke-linecap="round"
|
|
12
|
+
stroke-linejoin="round"
|
|
13
|
+
>
|
|
14
|
+
<path d="M12 17v-3a1 1 0 011-1h6" />
|
|
15
|
+
<path d="M19 17h-9a1 1 0 00-1 1v3" />
|
|
16
|
+
<path d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16" />
|
|
17
|
+
<path d="M19 9h-3a1 1 0 00-1 1v3" />
|
|
18
|
+
<path d="M22 21H2" />
|
|
19
|
+
</svg>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!-- @license lucide-static v1.68.0 - ISC -->
|
|
2
|
+
<svg
|
|
3
|
+
class="lucide lucide-globe"
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
width="24"
|
|
6
|
+
height="24"
|
|
7
|
+
viewBox="0 0 24 24"
|
|
8
|
+
fill="none"
|
|
9
|
+
stroke="currentColor"
|
|
10
|
+
stroke-width="2"
|
|
11
|
+
stroke-linecap="round"
|
|
12
|
+
stroke-linejoin="round"
|
|
13
|
+
>
|
|
14
|
+
<circle cx="12" cy="12" r="10" />
|
|
15
|
+
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
|
|
16
|
+
<path d="M2 12h20" />
|
|
17
|
+
</svg>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!-- @license lucide-static v1.68.0 - ISC -->
|
|
2
|
+
<svg
|
|
3
|
+
class="lucide lucide-hammer"
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
width="24"
|
|
6
|
+
height="24"
|
|
7
|
+
viewBox="0 0 24 24"
|
|
8
|
+
fill="none"
|
|
9
|
+
stroke="currentColor"
|
|
10
|
+
stroke-width="2"
|
|
11
|
+
stroke-linecap="round"
|
|
12
|
+
stroke-linejoin="round"
|
|
13
|
+
>
|
|
14
|
+
<path d="m15 12-9.373 9.373a1 1 0 0 1-3.001-3L12 9" />
|
|
15
|
+
<path d="m18 15 4-4" />
|
|
16
|
+
<path d="m21.5 11.5-1.914-1.914A2 2 0 0 1 19 8.172v-.344a2 2 0 0 0-.586-1.414l-1.657-1.657A6 6 0 0 0 12.516 3H9l1.243 1.243A6 6 0 0 1 12 8.485V10l2 2h1.172a2 2 0 0 1 1.414.586L18.5 14.5" />
|
|
17
|
+
</svg>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!-- @license lucide-static v1.68.0 - ISC -->
|
|
2
|
+
<svg
|
|
3
|
+
class="lucide lucide-inbox"
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
width="24"
|
|
6
|
+
height="24"
|
|
7
|
+
viewBox="0 0 24 24"
|
|
8
|
+
fill="none"
|
|
9
|
+
stroke="currentColor"
|
|
10
|
+
stroke-width="2"
|
|
11
|
+
stroke-linecap="round"
|
|
12
|
+
stroke-linejoin="round"
|
|
13
|
+
>
|
|
14
|
+
<polyline points="22 12 16 12 14 15 10 15 8 12 2 12" />
|
|
15
|
+
<path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" />
|
|
16
|
+
</svg>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!-- @license lucide-static v1.68.0 - ISC -->
|
|
2
|
+
<svg
|
|
3
|
+
class="lucide lucide-key-round"
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
width="24"
|
|
6
|
+
height="24"
|
|
7
|
+
viewBox="0 0 24 24"
|
|
8
|
+
fill="none"
|
|
9
|
+
stroke="currentColor"
|
|
10
|
+
stroke-width="2"
|
|
11
|
+
stroke-linecap="round"
|
|
12
|
+
stroke-linejoin="round"
|
|
13
|
+
>
|
|
14
|
+
<path d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z" />
|
|
15
|
+
<circle cx="16.5" cy="7.5" r=".5" fill="currentColor" />
|
|
16
|
+
</svg>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!-- @license lucide-static v1.68.0 - ISC -->
|
|
2
|
+
<svg
|
|
3
|
+
class="lucide lucide-layout-grid"
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
width="24"
|
|
6
|
+
height="24"
|
|
7
|
+
viewBox="0 0 24 24"
|
|
8
|
+
fill="none"
|
|
9
|
+
stroke="currentColor"
|
|
10
|
+
stroke-width="2"
|
|
11
|
+
stroke-linecap="round"
|
|
12
|
+
stroke-linejoin="round"
|
|
13
|
+
>
|
|
14
|
+
<rect width="7" height="7" x="3" y="3" rx="1" />
|
|
15
|
+
<rect width="7" height="7" x="14" y="3" rx="1" />
|
|
16
|
+
<rect width="7" height="7" x="14" y="14" rx="1" />
|
|
17
|
+
<rect width="7" height="7" x="3" y="14" rx="1" />
|
|
18
|
+
</svg>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!-- @license lucide-static v1.68.0 - ISC -->
|
|
2
|
+
<svg
|
|
3
|
+
class="lucide lucide-log-out"
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
width="24"
|
|
6
|
+
height="24"
|
|
7
|
+
viewBox="0 0 24 24"
|
|
8
|
+
fill="none"
|
|
9
|
+
stroke="currentColor"
|
|
10
|
+
stroke-width="2"
|
|
11
|
+
stroke-linecap="round"
|
|
12
|
+
stroke-linejoin="round"
|
|
13
|
+
>
|
|
14
|
+
<path d="m16 17 5-5-5-5" />
|
|
15
|
+
<path d="M21 12H9" />
|
|
16
|
+
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
|
17
|
+
</svg>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!-- @license lucide-static v1.68.0 - ISC -->
|
|
2
|
+
<svg
|
|
3
|
+
class="lucide lucide-message-circle"
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
width="24"
|
|
6
|
+
height="24"
|
|
7
|
+
viewBox="0 0 24 24"
|
|
8
|
+
fill="none"
|
|
9
|
+
stroke="currentColor"
|
|
10
|
+
stroke-width="2"
|
|
11
|
+
stroke-linecap="round"
|
|
12
|
+
stroke-linejoin="round"
|
|
13
|
+
>
|
|
14
|
+
<path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719" />
|
|
15
|
+
</svg>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!-- @license lucide-static v1.68.0 - ISC -->
|
|
2
|
+
<svg
|
|
3
|
+
class="lucide lucide-package"
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
width="24"
|
|
6
|
+
height="24"
|
|
7
|
+
viewBox="0 0 24 24"
|
|
8
|
+
fill="none"
|
|
9
|
+
stroke="currentColor"
|
|
10
|
+
stroke-width="2"
|
|
11
|
+
stroke-linecap="round"
|
|
12
|
+
stroke-linejoin="round"
|
|
13
|
+
>
|
|
14
|
+
<path d="M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z" />
|
|
15
|
+
<path d="M12 22V12" />
|
|
16
|
+
<polyline points="3.29 7 12 12 20.71 7" />
|
|
17
|
+
<path d="m7.5 4.27 9 5.15" />
|
|
18
|
+
</svg>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<!-- @license lucide-static v1.68.0 - ISC -->
|
|
2
|
+
<svg
|
|
3
|
+
class="lucide lucide-palette"
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
width="24"
|
|
6
|
+
height="24"
|
|
7
|
+
viewBox="0 0 24 24"
|
|
8
|
+
fill="none"
|
|
9
|
+
stroke="currentColor"
|
|
10
|
+
stroke-width="2"
|
|
11
|
+
stroke-linecap="round"
|
|
12
|
+
stroke-linejoin="round"
|
|
13
|
+
>
|
|
14
|
+
<path d="M12 22a1 1 0 0 1 0-20 10 9 0 0 1 10 9 5 5 0 0 1-5 5h-2.25a1.75 1.75 0 0 0-1.4 2.8l.3.4a1.75 1.75 0 0 1-1.4 2.8z" />
|
|
15
|
+
<circle cx="13.5" cy="6.5" r=".5" fill="currentColor" />
|
|
16
|
+
<circle cx="17.5" cy="10.5" r=".5" fill="currentColor" />
|
|
17
|
+
<circle cx="6.5" cy="12.5" r=".5" fill="currentColor" />
|
|
18
|
+
<circle cx="8.5" cy="7.5" r=".5" fill="currentColor" />
|
|
19
|
+
</svg>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!-- @license lucide-static v1.68.0 - ISC -->
|
|
2
|
+
<svg
|
|
3
|
+
class="lucide lucide-plug"
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
width="24"
|
|
6
|
+
height="24"
|
|
7
|
+
viewBox="0 0 24 24"
|
|
8
|
+
fill="none"
|
|
9
|
+
stroke="currentColor"
|
|
10
|
+
stroke-width="2"
|
|
11
|
+
stroke-linecap="round"
|
|
12
|
+
stroke-linejoin="round"
|
|
13
|
+
>
|
|
14
|
+
<path d="M12 22v-5" />
|
|
15
|
+
<path d="M15 8V2" />
|
|
16
|
+
<path d="M17 8a1 1 0 0 1 1 1v4a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1z" />
|
|
17
|
+
<path d="M9 8V2" />
|
|
18
|
+
</svg>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!-- @license lucide-static v1.68.0 - ISC -->
|
|
2
|
+
<svg
|
|
3
|
+
class="lucide lucide-rocket"
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
width="24"
|
|
6
|
+
height="24"
|
|
7
|
+
viewBox="0 0 24 24"
|
|
8
|
+
fill="none"
|
|
9
|
+
stroke="currentColor"
|
|
10
|
+
stroke-width="2"
|
|
11
|
+
stroke-linecap="round"
|
|
12
|
+
stroke-linejoin="round"
|
|
13
|
+
>
|
|
14
|
+
<path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" />
|
|
15
|
+
<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09" />
|
|
16
|
+
<path d="M9 12a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.4 22.4 0 0 1-4 2z" />
|
|
17
|
+
<path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 .05 5 .05" />
|
|
18
|
+
</svg>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!-- @license lucide-static v1.68.0 - ISC -->
|
|
2
|
+
<svg
|
|
3
|
+
class="lucide lucide-user"
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
width="24"
|
|
6
|
+
height="24"
|
|
7
|
+
viewBox="0 0 24 24"
|
|
8
|
+
fill="none"
|
|
9
|
+
stroke="currentColor"
|
|
10
|
+
stroke-width="2"
|
|
11
|
+
stroke-linecap="round"
|
|
12
|
+
stroke-linejoin="round"
|
|
13
|
+
>
|
|
14
|
+
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" />
|
|
15
|
+
<circle cx="12" cy="7" r="4" />
|
|
16
|
+
</svg>
|
data/lib/studio/icons.rb
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "erb"
|
|
4
|
+
|
|
5
|
+
# The engine's one line-icon set: Lucide, vendored under lib/studio/icons/lucide (see the
|
|
6
|
+
# README beside it for the license and how to add one). Pure Ruby, so the unit suite reads
|
|
7
|
+
# it without booting the dummy app. Views call StudioIconHelper#studio_icon.
|
|
8
|
+
#
|
|
9
|
+
# A NAME IS LOOKED UP, NEVER BUILT INTO A PATH. Only a name in the directory listing
|
|
10
|
+
# reaches File.read, so a host passing a bad name gets nil and cannot read another file.
|
|
11
|
+
module Studio
|
|
12
|
+
module Icons
|
|
13
|
+
DIR = File.expand_path("icons/lucide", __dir__)
|
|
14
|
+
# Lucide draws at 2 on a 24 box. At 18px that reads heavy beside 13.5px text, so the
|
|
15
|
+
# set renders thinner, as the sidebar's approved mockup did (2 x 18/24 is 1.5).
|
|
16
|
+
STROKE_WIDTH = 1.6
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
def names
|
|
21
|
+
@names ||= Dir.children(DIR).filter_map { |file| file.delete_suffix(".svg") if file.end_with?(".svg") }.sort.freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def known?(name)
|
|
25
|
+
names.include?(name.to_s)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The whole <svg>, or nil for a name the set does not hold. Decorative: the row or
|
|
29
|
+
# button beside it carries the words.
|
|
30
|
+
def svg(name, size: 18, css_class: nil)
|
|
31
|
+
return nil unless known?(name)
|
|
32
|
+
|
|
33
|
+
px = Integer(size)
|
|
34
|
+
attributes = [
|
|
35
|
+
%(xmlns="http://www.w3.org/2000/svg"),
|
|
36
|
+
%(width="#{px}" height="#{px}"),
|
|
37
|
+
%(viewBox="0 0 24 24"),
|
|
38
|
+
%(fill="none" stroke="currentColor" stroke-width="#{STROKE_WIDTH}"),
|
|
39
|
+
%(stroke-linecap="round" stroke-linejoin="round"),
|
|
40
|
+
%(aria-hidden="true" focusable="false"),
|
|
41
|
+
(%(class="#{ERB::Util.html_escape(css_class)}") if css_class.to_s != "")
|
|
42
|
+
].compact.join(" ")
|
|
43
|
+
"<svg #{attributes}>#{shapes(name.to_s)}</svg>"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The drawing inside the file's <svg>, read once per name.
|
|
47
|
+
def shapes(name)
|
|
48
|
+
@shapes ||= {}
|
|
49
|
+
@shapes[name] ||= File.read(File.join(DIR, "#{name}.svg"))[%r{<svg[^>]*>(.*)</svg>}m, 1].to_s.gsub(/\s*\n\s*/, "").freeze
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
data/lib/studio/loader.rb
CHANGED
|
@@ -27,6 +27,13 @@ module Studio
|
|
|
27
27
|
module Loader
|
|
28
28
|
class InvalidConfig < ArgumentError; end
|
|
29
29
|
|
|
30
|
+
# The one pace of a page load, for every app (docs/LOADER.md). A visit under the delay
|
|
31
|
+
# shows no overlay and waits for nothing. An overlay that shows stays the minimum.
|
|
32
|
+
# studio/loader falls back to the same two numbers; test/views/loader_render_test.rb
|
|
33
|
+
# holds them level.
|
|
34
|
+
OVERLAY_DELAY_MS = 350
|
|
35
|
+
OVERLAY_MIN_SHOW_MS = 750
|
|
36
|
+
|
|
30
37
|
SETTING = "Studio.loader"
|
|
31
38
|
GENERIC_STYLES = %i[breathe liquid orbit steel].freeze
|
|
32
39
|
ART_STYLES = { weld_bead: "outline", sparkle: "spark" }.freeze
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
3
5
|
# Resolves Studio.sidebar_sections — the host's declared link-sidebar data —
|
|
4
6
|
# for a given view context. Pure Ruby (no Rails dependency) so the unit suite
|
|
5
7
|
# exercises the resolution rules without booting the dummy app.
|
|
@@ -8,7 +10,7 @@
|
|
|
8
10
|
# context) for dynamic data: route helpers, logged_in? walls, model-backed
|
|
9
11
|
# link lists. Each section normalizes to symbol keys:
|
|
10
12
|
#
|
|
11
|
-
# { title: "Site", admin: true, links: [
|
|
13
|
+
# { title: "Site", admin: true, header: true, links: [
|
|
12
14
|
# { label: "Dashboard", href: "/admin", emoji: "📊",
|
|
13
15
|
# hover_emoji: "🔬", desc: "Users + logs", target: "_blank" } ] }
|
|
14
16
|
#
|
|
@@ -16,8 +18,13 @@
|
|
|
16
18
|
# trigger and panel stay invisible to everyone else even when the host
|
|
17
19
|
# declares nothing but admin links.
|
|
18
20
|
#
|
|
19
|
-
# A section's title is its group's accessible name
|
|
20
|
-
#
|
|
21
|
+
# A section's title is its group's accessible name. By default the panel shows no label
|
|
22
|
+
# word; a section with header: true also shows its title above its rows. A link's desc is
|
|
23
|
+
# optional and shows only on hover or keyboard focus.
|
|
24
|
+
#
|
|
25
|
+
# Under Studio.sidebar_style = :glass a link may also carry icon: "rocket", a Studio::Icons
|
|
26
|
+
# name: the row draws that line icon in place of its emoji, with no hover swap. The emoji
|
|
27
|
+
# stays the fallback for a name the set does not hold.
|
|
21
28
|
#
|
|
22
29
|
# THE ENGINE PREPENDS ONE SECTION OF ITS OWN — "You", linking to /profile. The
|
|
23
30
|
# engine ships that page, so it ships the way in rather than asking five apps to
|
|
@@ -25,8 +32,49 @@
|
|
|
25
32
|
# gates it carries.
|
|
26
33
|
module Studio
|
|
27
34
|
module SidebarSections
|
|
35
|
+
STYLES = %i[default glass].freeze
|
|
36
|
+
|
|
28
37
|
module_function
|
|
29
38
|
|
|
39
|
+
# The value Studio.sidebar_style= stores, or ArgumentError. A misspelt style would
|
|
40
|
+
# otherwise draw the default panel with no word about why.
|
|
41
|
+
def style!(value)
|
|
42
|
+
style = value.respond_to?(:to_sym) ? value.to_sym : value
|
|
43
|
+
return style if STYLES.include?(style)
|
|
44
|
+
|
|
45
|
+
raise ArgumentError, "Studio.sidebar_style must be one of #{STYLES.join(', ')}, got #{value.inspect}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The href of the row for the page at `path`, or nil. One row at most: the longest
|
|
49
|
+
# match wins, so /build/requests lights App Requests and not Build. A row also owns
|
|
50
|
+
# the pages beneath it (/books/2026 lights Books). "/" only ever matches itself. An absolute href counts only on `host`, so a satellite's /docs
|
|
51
|
+
# never lights here.
|
|
52
|
+
def current_href(sections, path, host: nil)
|
|
53
|
+
return nil if path.to_s.empty?
|
|
54
|
+
|
|
55
|
+
here = path.to_s[/\A[^?#]*/].chomp("/")
|
|
56
|
+
best = Array(sections).flat_map { |section| Array(section[:links]) }.filter_map do |link|
|
|
57
|
+
target = link_path(link[:href], host)
|
|
58
|
+
next if target.nil?
|
|
59
|
+
next unless target == here || (!target.empty? && here.start_with?("#{target}/"))
|
|
60
|
+
|
|
61
|
+
[target.length, link[:href]]
|
|
62
|
+
end.max_by(&:first)
|
|
63
|
+
best&.last
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# An href's local path without its trailing slash, or nil when it leads elsewhere or
|
|
67
|
+
# to nothing here (another host, a bare #anchor, a mailto:).
|
|
68
|
+
def link_path(href, host)
|
|
69
|
+
uri = URI.parse(href.to_s)
|
|
70
|
+
return nil if uri.path.to_s.empty? || uri.opaque
|
|
71
|
+
return nil if uri.host && uri.host != host
|
|
72
|
+
|
|
73
|
+
uri.path.chomp("/")
|
|
74
|
+
rescue URI::Error
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
|
|
30
78
|
def resolve(declared, view)
|
|
31
79
|
sections = declared.respond_to?(:call) ? declared.call(view) : declared
|
|
32
80
|
admin = view.respond_to?(:admin?) && view.admin?
|
|
@@ -60,7 +108,7 @@ module Studio
|
|
|
60
108
|
return [] unless profile_link?(view)
|
|
61
109
|
|
|
62
110
|
[{ title: "You", links: [
|
|
63
|
-
{ label: "Profile", href: view.profile_path, emoji: "👤" }
|
|
111
|
+
{ label: "Profile", href: view.profile_path, emoji: "👤", icon: "user" }
|
|
64
112
|
] }]
|
|
65
113
|
end
|
|
66
114
|
|
data/lib/studio/site_footer.rb
CHANGED
|
@@ -386,15 +386,36 @@ module Studio
|
|
|
386
386
|
# means the app has no chat, and `allowed` (a callable taking the viewer, nil
|
|
387
387
|
# when signed out) can hide it, e.g. admins only while testing. A hook that
|
|
388
388
|
# raises hides the entry and logs; the footer is on every page, and a link
|
|
389
|
-
# meant for a few must not show because its gate broke.
|
|
389
|
+
# meant for a few must not show because its gate broke. So does a path that
|
|
390
|
+
# raises: without the path there is no link, and the page still renders.
|
|
390
391
|
def chat_row(view, path:, label: nil, allowed: nil)
|
|
391
|
-
href =
|
|
392
|
+
href = chat_href(view, path)
|
|
392
393
|
return nil if text(href).nil?
|
|
393
394
|
return nil unless chat_allowed?(view, allowed)
|
|
394
395
|
|
|
395
396
|
[text(label) || "Chat", href]
|
|
396
397
|
end
|
|
397
398
|
|
|
399
|
+
def chat_href(view, path)
|
|
400
|
+
path.respond_to?(:call) ? path.call(view) : path
|
|
401
|
+
rescue StandardError => error
|
|
402
|
+
SiteFooter.reporter.call("[studio.site_footer] site_footer_chat_path raised #{error.class}: the Chat link is hidden")
|
|
403
|
+
capture_once(error)
|
|
404
|
+
nil
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# ErrorLog is a row per call, and the footer is on every page: one per error class.
|
|
408
|
+
def capture_once(error)
|
|
409
|
+
key = "site_footer_chat_path #{error.class}"
|
|
410
|
+
return if SiteFooter.reported.key?(key) || SiteFooter.reported.size >= REPORTED_LIMIT
|
|
411
|
+
return unless defined?(::ErrorLog) && ::ErrorLog.respond_to?(:capture!)
|
|
412
|
+
|
|
413
|
+
SiteFooter.reported[key] = true
|
|
414
|
+
::ErrorLog.capture!(error)
|
|
415
|
+
rescue StandardError
|
|
416
|
+
nil # reporting is best-effort: the footer still renders
|
|
417
|
+
end
|
|
418
|
+
|
|
398
419
|
def chat_allowed?(view, allowed)
|
|
399
420
|
return true if allowed.nil?
|
|
400
421
|
|
data/lib/studio/version.rb
CHANGED
data/lib/studio.rb
CHANGED
|
@@ -33,6 +33,7 @@ require "studio/js_identifier"
|
|
|
33
33
|
require "studio/hold_button"
|
|
34
34
|
require "studio/partial_block"
|
|
35
35
|
require "studio/sidebar_sections"
|
|
36
|
+
require "studio/icons"
|
|
36
37
|
require "studio/site_footer"
|
|
37
38
|
require "studio/marketing_kit"
|
|
38
39
|
require "studio/booking"
|
|
@@ -177,6 +178,22 @@ module Studio
|
|
|
177
178
|
@@page_transition_direction = direction
|
|
178
179
|
end
|
|
179
180
|
|
|
181
|
+
# One of :slide or :unfold. The ONE style this app's pages play, on every page change and
|
|
182
|
+
# every full load. Alex McRitchie's ruling, 2026-11-04: one app, one way, nothing random. The
|
|
183
|
+
# default is :slide, the plainer of the two, so a host that sets only a direction gets one
|
|
184
|
+
# steady move and never a draw. It does nothing without a direction, and nil means the default.
|
|
185
|
+
PAGE_TRANSITION_STYLES = %i[slide unfold].freeze
|
|
186
|
+
mattr_reader :page_transition_style, default: :slide
|
|
187
|
+
|
|
188
|
+
def self.page_transition_style=(value)
|
|
189
|
+
style = value.presence&.to_sym || :slide
|
|
190
|
+
unless PAGE_TRANSITION_STYLES.include?(style)
|
|
191
|
+
raise ArgumentError, "Studio.page_transition_style must be one of " \
|
|
192
|
+
"#{PAGE_TRANSITION_STYLES.join(', ')} (got #{value.inspect})"
|
|
193
|
+
end
|
|
194
|
+
@@page_transition_style = style
|
|
195
|
+
end
|
|
196
|
+
|
|
180
197
|
# Minimum ms the nav spinner stays visible once shown (_head.html.erb).
|
|
181
198
|
# Apps wrapping multi-second operations in the spinner (e.g. Solana RPC)
|
|
182
199
|
# keep the high default to avoid flicker; smooth-load apps typically drop
|
|
@@ -259,6 +276,37 @@ module Studio
|
|
|
259
276
|
# end
|
|
260
277
|
mattr_accessor :sidebar_sections, default: []
|
|
261
278
|
|
|
279
|
+
# Whether the link sidebar draws its own Geo row for an admin. Default true. False
|
|
280
|
+
# takes the row out of this panel only: components/_geo_signpost stays, so a host
|
|
281
|
+
# that moves Geo elsewhere renders the partial on that page.
|
|
282
|
+
mattr_accessor :sidebar_geo_signpost, default: true
|
|
283
|
+
|
|
284
|
+
# How the link sidebar's panel looks. :default is the full-height drawer every app has
|
|
285
|
+
# today. :glass floats an inset, translucent panel over the page, with line icons
|
|
286
|
+
# (a link's icon:), a marked current page and no separate blurred edge. It is opt-in,
|
|
287
|
+
# so an app moves only when it asks. engine.css, THE GLASS SIDEBAR, holds the look.
|
|
288
|
+
mattr_reader :sidebar_style, default: :default
|
|
289
|
+
|
|
290
|
+
def self.sidebar_style=(value)
|
|
291
|
+
@@sidebar_style = SidebarSections.style!(value)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# ---- Surfaces ----
|
|
295
|
+
# How cards look. :glass makes them translucent over a soft ambient page, once the host
|
|
296
|
+
# layout says so on its root: <html data-surface-style="<%= Studio.surface_style %>">.
|
|
297
|
+
# :default is what every app has today. engine.css, THE GLASS SURFACES, holds the look.
|
|
298
|
+
SURFACE_STYLES = %i[default glass].freeze
|
|
299
|
+
mattr_reader :surface_style, default: :default
|
|
300
|
+
|
|
301
|
+
def self.surface_style=(value)
|
|
302
|
+
style = value.respond_to?(:to_sym) ? value.to_sym : value
|
|
303
|
+
unless SURFACE_STYLES.include?(style)
|
|
304
|
+
raise ArgumentError, "Studio.surface_style must be one of #{SURFACE_STYLES.join(', ')}, got #{value.inspect}"
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
@@surface_style = style
|
|
308
|
+
end
|
|
309
|
+
|
|
262
310
|
# ---- Navbar links ----
|
|
263
311
|
# Links for the engine navbar's desktop bar and phone row. The default []
|
|
264
312
|
# renders nothing, so the navbar stays byte-identical until an app opts in.
|
|
@@ -338,6 +386,10 @@ module Studio
|
|
|
338
386
|
# art: "/brand/loader-sparkle.svg" }
|
|
339
387
|
mattr_reader :loader, default: Loader.normalize(nil)
|
|
340
388
|
|
|
389
|
+
# The page-load overlay reaches every page through layouts/studio/_head, at the one
|
|
390
|
+
# standard pace in Studio::Loader. false opts this app out. docs/LOADER.md.
|
|
391
|
+
mattr_accessor :loader_overlay, default: true
|
|
392
|
+
|
|
341
393
|
def self.loader=(config)
|
|
342
394
|
@@loader = Loader.normalize(config)
|
|
343
395
|
end
|
|
@@ -1802,8 +1854,8 @@ module Studio
|
|
|
1802
1854
|
# config.site_footer_chat_label = "Chat" # the default
|
|
1803
1855
|
# config.site_footer_chat_allowed = ->(user) { user&.admin? } # default: Studio.agent_chat_allowed
|
|
1804
1856
|
#
|
|
1805
|
-
# site_footer_chat_allowed takes the viewer, nil when signed out; a hook
|
|
1806
|
-
# raises hides the entry and logs (SiteFooter.chat_row). Unset, the link follows
|
|
1857
|
+
# site_footer_chat_allowed takes the viewer, nil when signed out; a path or a hook
|
|
1858
|
+
# that raises hides the entry and logs (SiteFooter.chat_row). Unset, the link follows
|
|
1807
1859
|
# Studio.agent_chat_allowed, so the link shows to whom the chat answers.
|
|
1808
1860
|
mattr_accessor :site_footer_chat_path, default: nil
|
|
1809
1861
|
mattr_accessor :site_footer_chat_label, default: "Chat"
|