studio-engine 0.127.0 → 0.129.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 +161 -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 +572 -37
- data/app/controllers/concerns/studio/error_handling.rb +17 -0
- 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 +88 -6
- data/app/javascript/studio/agent_chat_voice.js +59 -5
- data/app/javascript/studio/alpine_stores.js +80 -22
- 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/registrations/new.html.erb +4 -3
- data/app/views/sessions/new.html.erb +4 -3
- data/app/views/studio/agent_chat/_chat.html.erb +2 -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 +2 -4
- data/app/views/style/_glass.html.erb +83 -0
- data/app/views/style/_host.html.erb +2 -10
- data/app/views/style/_modals.html.erb +38 -231
- data/app/views/style/_scrims.html.erb +2 -9
- data/app/views/style/_site_footer.html.erb +5 -13
- data/app/views/style/_tasks.html.erb +16 -27
- data/app/views/style/_theme.html.erb +56 -78
- 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,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"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: studio-engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.129.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex McRitchie
|
|
@@ -489,6 +489,7 @@ files:
|
|
|
489
489
|
- app/helpers/studio/public_user_helper.rb
|
|
490
490
|
- app/helpers/studio/site_footer_helper.rb
|
|
491
491
|
- app/helpers/studio_email_delivery_helper.rb
|
|
492
|
+
- app/helpers/studio_icon_helper.rb
|
|
492
493
|
- app/helpers/studio_navbar_helper.rb
|
|
493
494
|
- app/helpers/studio_sidebar_helper.rb
|
|
494
495
|
- app/helpers/studio_theme_helper.rb
|
|
@@ -790,6 +791,7 @@ files:
|
|
|
790
791
|
- app/views/studio/voice/_recorder.html.erb
|
|
791
792
|
- app/views/studio/voice/_styles.html.erb
|
|
792
793
|
- app/views/style/_components.html.erb
|
|
794
|
+
- app/views/style/_glass.html.erb
|
|
793
795
|
- app/views/style/_host.html.erb
|
|
794
796
|
- app/views/style/_modal_specimen.html.erb
|
|
795
797
|
- app/views/style/_modals.html.erb
|
|
@@ -799,6 +801,7 @@ files:
|
|
|
799
801
|
- app/views/style/_tasks.html.erb
|
|
800
802
|
- app/views/style/_theme.html.erb
|
|
801
803
|
- app/views/style/_tricks.html.erb
|
|
804
|
+
- app/views/style/_type_ladder.html.erb
|
|
802
805
|
- app/views/style/board/_demo_card.html.erb
|
|
803
806
|
- app/views/style/index.html.erb
|
|
804
807
|
- app/views/style/modals/_age_gate.html.erb
|
|
@@ -864,6 +867,26 @@ files:
|
|
|
864
867
|
- lib/studio/health_report.rb
|
|
865
868
|
- lib/studio/hold_button.rb
|
|
866
869
|
- lib/studio/host_schema.rb
|
|
870
|
+
- lib/studio/icons.rb
|
|
871
|
+
- lib/studio/icons/README.md
|
|
872
|
+
- lib/studio/icons/lucide/LICENSE
|
|
873
|
+
- lib/studio/icons/lucide/activity.svg
|
|
874
|
+
- lib/studio/icons/lucide/book-open.svg
|
|
875
|
+
- lib/studio/icons/lucide/chart-column.svg
|
|
876
|
+
- lib/studio/icons/lucide/diamond.svg
|
|
877
|
+
- lib/studio/icons/lucide/door-stairwell.svg
|
|
878
|
+
- lib/studio/icons/lucide/globe.svg
|
|
879
|
+
- lib/studio/icons/lucide/hammer.svg
|
|
880
|
+
- lib/studio/icons/lucide/inbox.svg
|
|
881
|
+
- lib/studio/icons/lucide/key-round.svg
|
|
882
|
+
- lib/studio/icons/lucide/layout-grid.svg
|
|
883
|
+
- lib/studio/icons/lucide/log-out.svg
|
|
884
|
+
- lib/studio/icons/lucide/message-circle.svg
|
|
885
|
+
- lib/studio/icons/lucide/package.svg
|
|
886
|
+
- lib/studio/icons/lucide/palette.svg
|
|
887
|
+
- lib/studio/icons/lucide/plug.svg
|
|
888
|
+
- lib/studio/icons/lucide/rocket.svg
|
|
889
|
+
- lib/studio/icons/lucide/user.svg
|
|
867
890
|
- lib/studio/image_cache.rb
|
|
868
891
|
- lib/studio/ip_locations.rb
|
|
869
892
|
- lib/studio/js_identifier.rb
|