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
|
@@ -5,6 +5,15 @@
|
|
|
5
5
|
positions against the header, not the viewport); the engine navbar does
|
|
6
6
|
this automatically when sections are declared. %>
|
|
7
7
|
<% sections = studio_sidebar_sections %>
|
|
8
|
+
<%# THE PANEL'S STYLE (Studio.sidebar_style). Under :glass a row draws its link's line icon
|
|
9
|
+
and the row for this page says aria-current, which is what lights it. The default style
|
|
10
|
+
reads neither, so an app that stays on it renders what it did. A caller may pass
|
|
11
|
+
sidebar_style: for one render; the engine lab does, since the setting is process-wide. %>
|
|
12
|
+
<% glass = (local_assigns[:sidebar_style] || Studio.sidebar_style) == :glass %>
|
|
13
|
+
<% sidebar_request = glass && respond_to?(:request) ? request : nil %>
|
|
14
|
+
<%# The engine's Geo row counts too, so no section row above it claims /admin/geo as its own. %>
|
|
15
|
+
<% current_rows = respond_to?(:admin_geo_path) ? sections + [{ links: [{ href: admin_geo_path }] }] : sections %>
|
|
16
|
+
<% current_href = sidebar_request && Studio::SidebarSections.current_href(current_rows, sidebar_request.path, host: sidebar_request.host) %>
|
|
8
17
|
<% sidebar_admin = respond_to?(:admin?) && admin? %>
|
|
9
18
|
<%# THE PANEL'S TITLE. Signed in, admin included: the name the navbar shows, from the
|
|
10
19
|
same call. Signed out: a greeting by the browser's local time, which the server
|
|
@@ -24,18 +33,28 @@
|
|
|
24
33
|
elsif !sidebar_signed_in
|
|
25
34
|
tag.p("Good morning", class: sidebar_title_class, data: { link_sidebar_title: "", link_sidebar_greeting: "" })
|
|
26
35
|
end %>
|
|
27
|
-
<%# THE GROUPS.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
<%# THE GROUPS. A divider separates them, and by default a group shows no label word. The
|
|
37
|
+
words stay as each group's accessible name, so a screen reader still announces them.
|
|
38
|
+
An empty section draws no group, or its two dividers would touch. No group shows the
|
|
39
|
+
admin chip.
|
|
40
|
+
|
|
41
|
+
A SECTION MAY OPT IN TO A VISIBLE HEADER with header: true: its title, in the small
|
|
42
|
+
label style, above its rows. The word on screen is hidden from assistive technology,
|
|
43
|
+
because the group's aria-label already says it. The label is not an id reference:
|
|
44
|
+
body_html renders in both panels, so an id here would be a duplicate. The header is
|
|
45
|
+
plain text, outside every row, so it takes no click and no row measures against it.
|
|
46
|
+
Its pt-3 and the group's py-1 make the 16px the old label had above it.
|
|
31
47
|
|
|
32
48
|
A ROW IS ONE LINE AT REST, with or without a desc:, so the spacing never changes. A
|
|
33
49
|
desc: fades in on hover or keyboard focus, and nothing moves to make room: it sits
|
|
34
50
|
absolutely in the row's padding as the label lifts by a transform. engine.css, THE
|
|
35
51
|
SIDEBAR ROW'S DESCRIPTION, holds the rules. A row with no desc: gains no attribute. %>
|
|
36
|
-
<% sidebar_group = lambda do |title:, admin:, first:, &block|
|
|
52
|
+
<% sidebar_group = lambda do |title:, admin:, first:, header: false, &block|
|
|
37
53
|
name = [title.presence, ("admin" if admin && !title.to_s.match?(/\badmin\b/i))].compact.join(", ").presence
|
|
38
|
-
tag.div(
|
|
54
|
+
header_html = tag.div(title, class: "px-4 pt-3 pb-1 label-upper font-bold truncate", aria: { hidden: true }, data: { sidebar_group_header: "" }) if header && title.present?
|
|
55
|
+
tag.div(role: "group", aria: { label: name }, class: ["py-1", ("border-t border-subtle" unless first || glass)]) do
|
|
56
|
+
safe_join([header_html, capture(&block)].compact)
|
|
57
|
+
end
|
|
39
58
|
end %>
|
|
40
59
|
<% close_action = "$store.sidebars.linkTreeOpen = false" %>
|
|
41
60
|
|
|
@@ -55,16 +74,20 @@
|
|
|
55
74
|
<% body_html = capture do %>
|
|
56
75
|
<nav class="flex-1 min-h-0 overflow-y-auto py-1" aria-label="Studio links">
|
|
57
76
|
<% sections.select { |section| section[:links].any? }.each_with_index do |section, index| %>
|
|
58
|
-
<%= sidebar_group.call(title: section[:title], admin: section[:admin], first: index.zero
|
|
77
|
+
<%= sidebar_group.call(title: section[:title], admin: section[:admin], first: index.zero?, header: section[:header]) do %>
|
|
59
78
|
<% section[:links].each do |link| %>
|
|
60
79
|
<% target = link[:target].presence %>
|
|
80
|
+
<% line_icon = (studio_icon(link[:icon]) if glass && link[:icon].present?) %>
|
|
61
81
|
<%= link_to link[:href],
|
|
62
82
|
class: "group flex items-center gap-3 px-4 py-3 text-sm text-body hover:text-heading hover:bg-surface-alt focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-inset transition",
|
|
63
83
|
target: target,
|
|
64
84
|
rel: target == "_blank" ? "noopener" : nil,
|
|
65
85
|
"data-sidebar-row-described": (link[:desc].present? ? "" : nil),
|
|
86
|
+
"aria-current": (current_href.present? && link[:href] == current_href ? "page" : nil),
|
|
66
87
|
"@click": close_action do %>
|
|
67
|
-
<% if
|
|
88
|
+
<% if line_icon %>
|
|
89
|
+
<span class="flex-shrink-0" data-sidebar-row-icon><%= line_icon %></span>
|
|
90
|
+
<% elsif link[:hover_emoji].present? %>
|
|
68
91
|
<%= render "components/emoji_swap",
|
|
69
92
|
base: link[:emoji],
|
|
70
93
|
hover: link[:hover_emoji],
|
|
@@ -119,8 +142,11 @@
|
|
|
119
142
|
rather than called: this partial renders in hosts and test rigs that
|
|
120
143
|
provide studio_sidebar_sections without the rest of the helper, and a
|
|
121
144
|
bare call would raise for them. The two must stay the same question —
|
|
122
|
-
if the helper's rule changes, this changes with it.
|
|
123
|
-
|
|
145
|
+
if the helper's rule changes, this changes with it.
|
|
146
|
+
|
|
147
|
+
A host that moves Geo to its own page sets Studio.sidebar_geo_signpost = false
|
|
148
|
+
and renders components/_geo_signpost there. %>
|
|
149
|
+
<% if Studio.sidebar_geo_signpost && sidebar_admin && sections.any? { |section| section[:admin] } %>
|
|
124
150
|
<%= sidebar_group.call(title: "Geo", admin: true, first: false) do %>
|
|
125
151
|
<%= render "components/geo_signpost", variant: :sidebar, close_action: close_action %>
|
|
126
152
|
<% end %>
|
|
@@ -130,11 +156,11 @@
|
|
|
130
156
|
|
|
131
157
|
<% footer_html = capture do %>
|
|
132
158
|
<% if logged_in? %>
|
|
133
|
-
<div class="border-t border-subtle p-2"
|
|
159
|
+
<div class="border-t border-subtle p-2"<% if glass %> data-sidebar-glass-foot<% end %>>
|
|
134
160
|
<%= link_to logout_path,
|
|
135
161
|
class: "flex items-center gap-3 rounded-md px-4 py-3 text-sm text-body hover:text-heading hover:bg-surface-alt focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 transition",
|
|
136
162
|
"@click": close_action do %>
|
|
137
|
-
|
|
163
|
+
<% if glass %><span class="flex-shrink-0" data-sidebar-row-icon><%= studio_icon("log-out") %></span><% else %><span>👋</span><% end %><span>Log out</span>
|
|
138
164
|
<% end %>
|
|
139
165
|
</div>
|
|
140
166
|
<% end %>
|
|
@@ -151,6 +177,7 @@
|
|
|
151
177
|
escape_action: close_action,
|
|
152
178
|
display_class: "hidden md:flex",
|
|
153
179
|
edge: true,
|
|
180
|
+
glass: glass,
|
|
154
181
|
role: "navigation",
|
|
155
182
|
aria_label: sidebar_admin ? "Admin menu" : "Links menu" %>
|
|
156
183
|
|
|
@@ -166,5 +193,6 @@
|
|
|
166
193
|
display_class: "flex md:hidden",
|
|
167
194
|
width_class: "w-full",
|
|
168
195
|
edge: true,
|
|
196
|
+
glass: glass,
|
|
169
197
|
role: "navigation",
|
|
170
198
|
aria_label: sidebar_admin ? "Admin menu" : "Links menu" %>
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
would cover the expanded header. engine.css (THE PINNED EDGE ON THE SCROLL
|
|
20
20
|
TIMELINE) moves it with the header on the scroll timeline instead.
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
Four opt-in locals (unset, the panel renders as it did in 0.113.0):
|
|
23
23
|
title_html the header's LEAD SLOT: HTML in the title's place, for a panel
|
|
24
24
|
whose lead is a live status line rather than a label. It is
|
|
25
25
|
content, NOT a name: it never feeds aria-labelledby or
|
|
@@ -30,6 +30,11 @@
|
|
|
30
30
|
panel's inner edge, drawn by engine.css (THE SIDEBAR'S EDGE) from md
|
|
31
31
|
up. It softens the page beside the panel, so it suits a menu that
|
|
32
32
|
opens over the page, not a panel kept open beside live content.
|
|
33
|
+
glass true floats an inset, translucent panel (engine.css, THE GLASS SIDEBAR).
|
|
34
|
+
The aside keeps its geometry, its slide and its pinned edge, and turns
|
|
35
|
+
into a clear frame around an inner shell, which holds the blur. A
|
|
36
|
+
backdrop-filter makes a containing block, and this keeps it off the
|
|
37
|
+
element Alpine and Turbo move. Glass ignores `edge`: the whole panel blurs.
|
|
33
38
|
close_hook true stamps the close button data-sidebar-panel-close="<id>"
|
|
34
39
|
too, so a host's own document handler can match the button
|
|
35
40
|
to its panel by id. Needs `id`; without one it adds nothing.
|
|
@@ -53,7 +58,9 @@
|
|
|
53
58
|
<% aria_label = local_assigns.fetch(:aria_label, title) %>
|
|
54
59
|
<% labelledby = id.present? && title.present? ? "#{id}-title" : nil %>
|
|
55
60
|
<% close_hook = local_assigns.fetch(:close_hook, false) && id.present? %>
|
|
56
|
-
<%
|
|
61
|
+
<% glass = local_assigns.fetch(:glass, false) %>
|
|
62
|
+
<% edge = local_assigns.fetch(:edge, false) && !glass %>
|
|
63
|
+
<% surface_class = glass ? "" : "bg-surface border-l border-subtle shadow-2xl " %>
|
|
57
64
|
|
|
58
65
|
<aside
|
|
59
66
|
<% if id.present? %>id="<%= id %>"<% end %>
|
|
@@ -78,17 +85,20 @@
|
|
|
78
85
|
x-transition:leave="transition ease-in duration-200"
|
|
79
86
|
x-transition:leave-start="translate-x-0"
|
|
80
87
|
x-transition:leave-end="translate-x-full"
|
|
81
|
-
class="<%= display_class %> fixed right-0 <%= width_class %>
|
|
88
|
+
class="<%= display_class %> fixed right-0 <%= width_class %> <%= surface_class %><%= z_class %> flex-col <%= panel_class %>"
|
|
82
89
|
style="top:var(--nav-bottom, var(--nav-h, 6rem));height:calc(100% - var(--nav-bottom, var(--nav-h, 6rem)))"
|
|
83
90
|
data-pin-edge
|
|
84
91
|
<% if edge %>
|
|
85
92
|
data-sidebar-edge
|
|
86
93
|
<% end %>
|
|
94
|
+
<% if glass %>
|
|
95
|
+
data-sidebar-glass
|
|
96
|
+
<% end %>
|
|
87
97
|
role="<%= role %>"
|
|
88
98
|
<% if labelledby %>aria-labelledby="<%= labelledby %>"<% elsif aria_label.present? %>aria-label="<%= aria_label %>"<% end %>>
|
|
89
|
-
<div class="flex flex-col h-full"
|
|
99
|
+
<div class="flex flex-col h-full"<% if glass %> data-sidebar-glass-shell<% end %>>
|
|
90
100
|
<% if title.present? || title_html.present? || actions_html.present? || close_action.present? %>
|
|
91
|
-
<div class="flex items-center justify-between p-4 border-b border-subtle"
|
|
101
|
+
<div class="flex items-center justify-between p-4 border-b border-subtle"<% if glass %> data-sidebar-glass-head<% end %>>
|
|
92
102
|
<% if title.present? %>
|
|
93
103
|
<h3 <% if labelledby %>id="<%= labelledby %>"<% end %> class="text-lg font-bold text-heading"><%= title %></h3>
|
|
94
104
|
<% elsif title_html.present? %>
|
|
@@ -198,6 +198,12 @@
|
|
|
198
198
|
<% if sticky_table_headers %>
|
|
199
199
|
<%= stylesheet_link_tag "studio/sticky_table_header", "data-turbo-track": "reload" %>
|
|
200
200
|
<% end %>
|
|
201
|
+
<%# THE PAGE-LOAD OVERLAY, FOR EVERY APP. The body is the host's, so the overlay waits
|
|
202
|
+
here, inert, and studio/loader adds it to the body when a visit starts. A host's own
|
|
203
|
+
studio_loader_overlay wins. Studio.loader_overlay = false opts out. docs/LOADER.md. %>
|
|
204
|
+
<% if Studio.loader_overlay && respond_to?(:studio_loader_overlay) %>
|
|
205
|
+
<template data-studio-loader-overlay-template><%= studio_loader_overlay %></template>
|
|
206
|
+
<% end %>
|
|
201
207
|
<%= javascript_importmap_tags %>
|
|
202
208
|
<%# EVERY ENGINE SCRIPT BELOW IS A MODULE, AND EVERY MODULE TAG COMES AFTER THE
|
|
203
209
|
IMPORT MAP. A browser without dynamic import maps ignores an import map that
|
|
@@ -207,12 +213,17 @@
|
|
|
207
213
|
carry no body and need no nonce under a policy that allows 'self'. The session
|
|
208
214
|
store (studio/_session_stamp, above) and the full-load script above are the two
|
|
209
215
|
classic scripts, and each says why.
|
|
210
|
-
A MODULE
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
216
|
+
A MODULE TAG BY URL SENDS NO PRELOAD HEADER. javascript_include_tag answers
|
|
217
|
+
type: "module" with a `Link: rel=modulepreload` response header unless the call
|
|
218
|
+
passes preload_links_header: false, and the browser acts on that header before it
|
|
219
|
+
parses this map. Firefox then refuses the map outright ("Import maps are not allowed
|
|
220
|
+
after a module load or preload has started"), so every bare studio/* import on the
|
|
221
|
+
page fails and no panel opens (task firefox-import-map-preload,
|
|
222
|
+
test/views/head_sends_no_module_preload_test.rb).
|
|
223
|
+
A MODULE LOADED BY URL ALSO IMPORTS NO BARE SPECIFIER. A preload that arrived some
|
|
224
|
+
other way would resolve it against no map and fail for good (0.115.1.rc1, task
|
|
214
225
|
confetti-resolves-on-hub). A module with bare imports takes a
|
|
215
|
-
javascript_import_module_tag
|
|
226
|
+
javascript_import_module_tag. %>
|
|
216
227
|
<%# THE ENGINE'S BROWSER BOOT (studio/application): its every-page Stimulus
|
|
217
228
|
controllers, the pinned stack, and the Alpine shims. A module tag, so it
|
|
218
229
|
carries the nonce. %>
|
|
@@ -311,7 +322,7 @@
|
|
|
311
322
|
assigns window.Alpine and starts Alpine in a microtask after it is evaluated,
|
|
312
323
|
which as a module is after every module above it. window.Alpine is not
|
|
313
324
|
deprecated; Alpine itself and every host read it. %>
|
|
314
|
-
<%= javascript_include_tag "studio/alpine", type: "module", "data-turbo-track": "reload" %>
|
|
325
|
+
<%= javascript_include_tag "studio/alpine", type: "module", preload_links_header: false, "data-turbo-track": "reload" %>
|
|
315
326
|
<% if sticky_table_headers %>
|
|
316
|
-
<%= javascript_include_tag "studio/sticky_table_header", type: "module", "data-turbo-track": "reload" %>
|
|
327
|
+
<%= javascript_include_tag "studio/sticky_table_header", type: "module", preload_links_header: false, "data-turbo-track": "reload" %>
|
|
317
328
|
<% end %>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
so a preview looks as it does in the app, and nothing else: no navbar, no
|
|
4
4
|
session, no script. -%>
|
|
5
5
|
<!DOCTYPE html>
|
|
6
|
-
<html
|
|
6
|
+
<html<%= %( data-surface-style="glass").html_safe if Studio.surface_style == :glass %>>
|
|
7
7
|
<head>
|
|
8
8
|
<meta charset="utf-8">
|
|
9
9
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
@@ -81,9 +81,10 @@
|
|
|
81
81
|
|
|
82
82
|
<% if Studio.auth_method?(:google) %>
|
|
83
83
|
<% if email_signup %>
|
|
84
|
-
<div class="
|
|
85
|
-
<div class="
|
|
86
|
-
<
|
|
84
|
+
<div class="flex items-center gap-3 my-6 text-sm">
|
|
85
|
+
<div class="flex-1 border-t border-subtle"></div>
|
|
86
|
+
<span class="text-secondary">or</span>
|
|
87
|
+
<div class="flex-1 border-t border-subtle"></div>
|
|
87
88
|
</div>
|
|
88
89
|
<% end %>
|
|
89
90
|
|
|
@@ -81,9 +81,10 @@
|
|
|
81
81
|
|
|
82
82
|
<% if Studio.auth_method?(:google) %>
|
|
83
83
|
<% if Studio.password_login_available? || Studio.auth_method?(:magic_link) %>
|
|
84
|
-
<div class="
|
|
85
|
-
<div class="
|
|
86
|
-
<
|
|
84
|
+
<div class="flex items-center gap-3 my-6 text-sm">
|
|
85
|
+
<div class="flex-1 border-t border-subtle"></div>
|
|
86
|
+
<span class="text-secondary">or</span>
|
|
87
|
+
<div class="flex-1 border-t border-subtle"></div>
|
|
87
88
|
</div>
|
|
88
89
|
<% end %>
|
|
89
90
|
|
|
@@ -42,8 +42,9 @@
|
|
|
42
42
|
<div class="flex flex-col gap-3 min-w-0" data-agent-chat-cards data-test="agent-chat-cards"><%= cards %></div>
|
|
43
43
|
<% end %>
|
|
44
44
|
<div class="flex flex-col gap-3 min-w-0">
|
|
45
|
+
<%# relative: a turn's sr-only name is absolutely positioned. With no positioned ancestor it escapes this scroller, is not clipped by it, and stretches the document by the thread's whole scrolled height. %>
|
|
45
46
|
<div role="log" aria-live="polite" aria-label="Conversation with <%= name %>"
|
|
46
|
-
class="flex flex-col gap-4 px-1 py-2 overflow-y-auto overscroll-contain max-h-[60vh] min-w-0"
|
|
47
|
+
class="relative flex flex-col gap-4 px-1 py-2 overflow-y-auto overscroll-contain max-h-[60vh] min-w-0"
|
|
47
48
|
data-agent-chat-log data-test="agent-chat-messages">
|
|
48
49
|
<p class="hidden only:block text-center text-secondary text-sm py-6" data-test="agent-chat-empty"><%= empty.presence || "Say hello to #{name}." %></p>
|
|
49
50
|
</div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<%# locals: (turn:, name:) %>
|
|
2
|
-
<%# One turn of the chat (studio/agent_chat/_chat), from a hub turn {id, role, text, status}, identified by its id AND role (data-turn-key "<id>:<role>"): the hub answers one message row as a user turn and an agent turn sharing the row's id. The page renders it twice, blank, as the <template>s studio/agent_chat clones; a host may also render real turns with it. The visitor's words sit in a right-hand bubble; the agent's are plain text across the thread (as the hub's agent-reply), its name kept for a screen reader. Text only, never HTML: the script sets textContent. A failed turn says so. %>
|
|
2
|
+
<%# One turn of the chat (studio/agent_chat/_chat), from a hub turn {id, role, text, status}, identified by its id AND role (data-turn-key "<id>:<role>"): the hub answers one message row as a user turn and an agent turn sharing the row's id. The page renders it twice, blank, as the <template>s studio/agent_chat clones; a host may also render real turns with it. The visitor's words sit in a right-hand bubble; the agent's are plain text across the thread (as the hub's agent-reply), its name kept for a screen reader. Text only, never HTML: the script sets textContent. A failed turn says so. A held one (stored, no automatic answer coming) says that, and never asks for a resend. %>
|
|
3
3
|
<% agent = turn["role"].to_s == "agent" %>
|
|
4
4
|
<% status = turn["status"].to_s %>
|
|
5
5
|
<div class="flex flex-col gap-2 min-w-0" data-agent-chat-turn data-role="<%= agent ? "agent" : "user" %>"
|
|
@@ -16,4 +16,6 @@
|
|
|
16
16
|
<% end %>
|
|
17
17
|
<p class="<%= agent ? "self-start" : "self-end" %> text-xs text-danger-ink" data-agent-chat-failed data-test="agent-chat-failed"
|
|
18
18
|
<%= "hidden" unless status == "failed" %>><%= name %> could not answer this one. Send it again.</p>
|
|
19
|
+
<p class="<%= agent ? "self-start" : "self-end" %> text-xs text-muted" data-agent-chat-held data-test="agent-chat-held"
|
|
20
|
+
<%= "hidden" unless status == "held" %>>Your message was received, but it won't get an automatic answer.</p>
|
|
19
21
|
</div>
|
|
@@ -7,16 +7,19 @@
|
|
|
7
7
|
No audio is kept. Status lines are set as text.
|
|
8
8
|
|
|
9
9
|
Scoped CSS for the reason studio/voice/_styles gives: it must render in an
|
|
10
|
-
app whose Tailwind build never saw this file.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
app whose Tailwind build never saw this file. A held button must not move, stuck
|
|
11
|
+
or not, so everything above and below it keeps its size through a hold: the
|
|
12
|
+
status line reserves the two lines the quiet hint wraps to at phone width (one
|
|
13
|
+
where the composer is wide enough), and holds its height through the hold
|
|
14
|
+
(holdLine, studio/agent_chat_voice); the chip is always one line, whatever name it
|
|
15
|
+
shows; the timer sits on the button, not beside it; and the recorder's own line
|
|
16
|
+
never shows here. e2e/agent_chat_voice.spec.js measures all of it at 390px. %>
|
|
15
17
|
<% words = Studio::AgentChat.voice_copy(copy) %>
|
|
16
18
|
<style>
|
|
17
|
-
.studio-agent-voice { display: grid; gap: .25rem; padding: 0 .25rem .25rem; min-width: 0; }
|
|
18
|
-
.studio-agent-voice-status { min-height:
|
|
19
|
+
.studio-agent-voice { display: grid; gap: .25rem; padding: 0 .25rem .25rem; min-width: 0; container-type: inline-size; }
|
|
20
|
+
.studio-agent-voice-status { min-height: 2.5rem; margin: 0; padding: 0 .5rem; font-size: .875rem; line-height: 1.25rem; overflow-wrap: anywhere;
|
|
19
21
|
color: var(--color-text-body, inherit); }
|
|
22
|
+
@container (min-width: 36rem) { .studio-agent-voice-status { min-height: 1.25rem; } }
|
|
20
23
|
.studio-agent-voice-failed { display: none; margin: 0; padding: 0 .5rem; font-size: .8125rem; color: var(--color-text-secondary, inherit); }
|
|
21
24
|
[data-studio-controller-failed~="agent-chat-voice"] > .studio-agent-voice-failed { display: block; }
|
|
22
25
|
/* One row where both fit; on a phone the chip takes the first line and the
|
|
@@ -24,18 +27,30 @@
|
|
|
24
27
|
.studio-agent-voice-row { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; min-width: 0; }
|
|
25
28
|
.studio-agent-voice-row .studio-mic { display: contents; }
|
|
26
29
|
.studio-agent-voice-row .studio-mic-chip { flex: 1 1 14rem; min-width: 0; }
|
|
27
|
-
.studio-agent-voice-row .studio-mic-
|
|
30
|
+
.studio-agent-voice-row .studio-mic-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
31
|
+
/* The composer sticks to the window's bottom, so what doesn't fit is cut off at the
|
|
32
|
+
top, the chip with it. The picker scrolls inside what the rest leaves it: 20rem is
|
|
33
|
+
the composer's 15rem at 390px, the privacy line under it, and a margin. */
|
|
34
|
+
.studio-agent-voice-row .studio-mic-panel { flex: 1 0 100%; max-width: none; order: 3; align-content: start; overflow-y: auto; overscroll-behavior: contain;
|
|
35
|
+
max-height: max(6rem, calc(100vh - 20rem)); max-height: max(6rem, calc(100dvh - 20rem)); }
|
|
28
36
|
.studio-agent-voice-row .studio-mic-failed { flex: 1 0 100%; order: 4; }
|
|
29
|
-
|
|
37
|
+
/* 14rem: the label and, either side of it, the timer's room. Narrower, the two overlap. */
|
|
38
|
+
.studio-agent-voice-row .studio-recorder { flex: 1 1 14rem; min-width: 0; flex-flow: row wrap; justify-content: flex-end; gap: .5rem; }
|
|
30
39
|
.studio-agent-voice-row .studio-recorder-button { flex: 1 1 10rem; min-width: 0; padding: .625rem 1rem; }
|
|
31
40
|
.studio-agent-voice-row .studio-recorder-timer { display: none; }
|
|
32
|
-
|
|
41
|
+
/* On the button, not beside it: beside it, it took 40px off a held button. */
|
|
42
|
+
.studio-agent-voice-row .studio-recorder { position: relative; }
|
|
43
|
+
.studio-agent-voice-row [data-voice-recorder-state="recording"] .studio-recorder-timer { display: inline; position: absolute; top: 50%; right: 1rem; transform: translateY(-50%);
|
|
44
|
+
pointer-events: none; color: var(--btn-primary-fg, #fff); }
|
|
33
45
|
.studio-agent-voice-row .studio-recorder-status { flex: 1 0 100%; min-height: 0; max-width: none; font-size: .875rem; }
|
|
34
|
-
/* The chat's status line speaks for these
|
|
35
|
-
|
|
46
|
+
/* The chat's status line speaks for these; a second live line would say them
|
|
47
|
+
twice, and its coming and going would move a held button. */
|
|
36
48
|
.studio-agent-voice-row .studio-recorder-status:empty,
|
|
37
49
|
.studio-agent-voice-row [data-voice-recorder-state="recording"] .studio-recorder-status,
|
|
38
|
-
.studio-agent-voice-row [data-voice-recorder-state="silent"] .studio-recorder-status
|
|
50
|
+
.studio-agent-voice-row [data-voice-recorder-state="silent"] .studio-recorder-status,
|
|
51
|
+
.studio-agent-voice-row [data-voice-recorder-state="failed"] .studio-recorder-status,
|
|
52
|
+
.studio-agent-voice-row [data-voice-recorder-state="denied"] .studio-recorder-status,
|
|
53
|
+
.studio-agent-voice-row [data-voice-recorder-state="tapped"] .studio-recorder-status { display: none; }
|
|
39
54
|
.studio-agent-voice-row .studio-recorder-failed { flex: 1 0 100%; }
|
|
40
55
|
</style>
|
|
41
56
|
<div class="studio-agent-voice" role="group" aria-label="Talk to <%= name %>"
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
<%# locals: (label:, delay:, max_wait:, size:, options: {}) %>
|
|
1
|
+
<%# locals: (label:, delay:, min_show:, max_wait:, size:, options: {}) %>
|
|
2
2
|
<%#
|
|
3
3
|
The page-load overlay (Studio::LoaderHelper#studio_loader_overlay). Rendered
|
|
4
4
|
hidden, once per page; studio/loader shows it when a visit the user started
|
|
5
|
-
or a form marked data-studio-loader-overlay outlasts data-delay ms,
|
|
6
|
-
after data-max-wait ms at most, and window.StudioLoader.show(caption) /
|
|
5
|
+
or a form marked data-studio-loader-overlay outlasts data-delay ms, keeps it
|
|
6
|
+
data-min-show ms once shown, clears it after data-max-wait ms at most, and window.StudioLoader.show(caption) /
|
|
7
7
|
hide() drive it for any other long wait. The caption line is empty until a
|
|
8
8
|
caller names one. It never marks itself aria-busy: that would mute the
|
|
9
9
|
"Loading" status inside it.
|
|
10
10
|
%>
|
|
11
|
-
<%= tag.div(class: "studio-loader-overlay", hidden: true, data: { studio_loader_overlay: "", delay: delay, max_wait: max_wait }) do %>
|
|
11
|
+
<%= tag.div(class: "studio-loader-overlay", hidden: true, data: { studio_loader_overlay: "", delay: delay, min_show: min_show, max_wait: max_wait }) do %>
|
|
12
12
|
<div class="studio-loader-overlay-panel">
|
|
13
13
|
<%= studio_loader(size: size, label: label, **options) %>
|
|
14
14
|
<p class="studio-loader-caption" data-studio-loader-caption></p>
|
|
@@ -7,10 +7,8 @@
|
|
|
7
7
|
to a 404. -%>
|
|
8
8
|
<section id="components" class="space-y-6" style="scroll-margin-top: calc(var(--nav-h, 0px) + 5rem)">
|
|
9
9
|
<div class="space-y-1">
|
|
10
|
-
<h2 class="
|
|
11
|
-
<p class="text-muted text-sm max-w-2xl">
|
|
12
|
-
The engine's ViewComponent classes, one preview per state, in the component gallery.
|
|
13
|
-
</p>
|
|
10
|
+
<h2 class="type-h3 text-heading">Components</h2>
|
|
11
|
+
<p class="text-muted text-sm max-w-2xl">One preview per state, in the component gallery.</p>
|
|
14
12
|
</div>
|
|
15
13
|
|
|
16
14
|
<% if Studio.lookbook_mounted? %>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<%# The glass surfaces, the glass sidebar and the line icons (Studio.surface_style,
|
|
2
|
+
Studio.sidebar_style, studio_icon). Real classes and the real helper, so a change to any
|
|
3
|
+
of them shows here. The stage has a hard-edged backing because glass needs something
|
|
4
|
+
behind it: on a flat page it looks like a flat tint. Each specimen says whether this app
|
|
5
|
+
has turned its switch on, because the surface rules only apply when it has. %>
|
|
6
|
+
<%
|
|
7
|
+
glass_backing = "rounded-xl p-4 [background-image:repeating-linear-gradient(135deg,rgb(var(--color-primary-rgb)/0.55)_0_14px,transparent_14px_28px)]"
|
|
8
|
+
surface_on = Studio.surface_style == :glass
|
|
9
|
+
sidebar_on = Studio.sidebar_style == :glass
|
|
10
|
+
%>
|
|
11
|
+
<section class="space-y-4" data-test="style-glass">
|
|
12
|
+
<div>
|
|
13
|
+
<h3 class="type-h4 text-heading">Glass</h3>
|
|
14
|
+
<p class="text-sm text-secondary mt-1">Each one is opt-in per app.</p>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="grid gap-4 md:grid-cols-2">
|
|
17
|
+
<%= render layout: "style/specimen",
|
|
18
|
+
locals: { klass: "Studio.surface_style = :glass",
|
|
19
|
+
usage: %(config.surface_style = :glass, and <html data-surface-style="glass">),
|
|
20
|
+
disabled: !surface_on, disabled_label: "surface style is off on this app" } do %>
|
|
21
|
+
<div class="<%= glass_backing %> w-full space-y-2" data-test="style-glass-surfaces">
|
|
22
|
+
<div class="card p-3 text-sm text-heading">.card, the dense tier: no blur</div>
|
|
23
|
+
<div class="card surface-glass-blur p-3 text-sm text-heading">.card.surface-glass-blur</div>
|
|
24
|
+
</div>
|
|
25
|
+
<% end %>
|
|
26
|
+
<%= render layout: "style/specimen",
|
|
27
|
+
locals: { klass: "Studio.sidebar_style = :glass",
|
|
28
|
+
usage: %(config.sidebar_style = :glass),
|
|
29
|
+
disabled: !sidebar_on, disabled_label: "glass sidebar is off on this app" } do %>
|
|
30
|
+
<div class="<%= glass_backing %> w-full" data-test="style-glass-sidebar">
|
|
31
|
+
<div class="flex flex-col" data-sidebar-glass-shell>
|
|
32
|
+
<nav aria-label="Glass sidebar specimen">
|
|
33
|
+
<div role="group" class="pt-2.5">
|
|
34
|
+
<a href="#tricks" class="group flex items-center" aria-current="page"><span class="flex-shrink-0" data-sidebar-row-icon><%= studio_icon("chart-column") %></span><span class="min-w-0"><span class="block truncate">Current page</span></span></a>
|
|
35
|
+
<a href="#tricks" class="group flex items-center" data-sidebar-row-described><span class="flex-shrink-0" data-sidebar-row-icon><%= studio_icon("rocket") %></span><span class="relative min-w-0 flex-1"><span class="block truncate" data-sidebar-row-label>Hover this row</span><span class="block truncate text-xs text-muted" data-sidebar-row-desc>Its description moves nothing</span></span></a>
|
|
36
|
+
</div>
|
|
37
|
+
</nav>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
<% if respond_to?(:studio_sidebar?) && studio_sidebar? %>
|
|
41
|
+
<button type="button" class="btn btn-sm btn-outline mt-3" @click="$store.sidebars.linkTreeOpen = true" data-test="style-glass-open">Open the real sidebar</button>
|
|
42
|
+
<% end %>
|
|
43
|
+
<% end %>
|
|
44
|
+
<%= render layout: "style/specimen",
|
|
45
|
+
locals: { klass: "Lit from above, under surface_style = :glass",
|
|
46
|
+
usage: %(.btn-primary, .btn-danger, .btn-neutral, .badge, .input-field),
|
|
47
|
+
disabled: !surface_on, disabled_label: "surface style is off on this app" } do %>
|
|
48
|
+
<div class="w-full overflow-x-auto" data-test="style-lit-states">
|
|
49
|
+
<table class="text-xs text-muted">
|
|
50
|
+
<thead><tr><th></th><% %w[rest hover pressed focus disabled].each do |state| %><th class="font-medium text-left"><%= state %></th><% end %></tr></thead>
|
|
51
|
+
<tbody>
|
|
52
|
+
<% [["btn-primary", "Primary"], ["btn-secondary", "Secondary"], ["btn-success", "Success"], ["btn-danger", "Danger"], ["btn-warning", "Warning"], ["btn-primary btn-sm", "Small"], ["btn-neutral btn-sm", "Neutral"], ["btn-outline btn-sm", "Outline"]].each do |classes, label| %>
|
|
53
|
+
<tr>
|
|
54
|
+
<th class="font-medium text-left pr-2"><%= label %></th>
|
|
55
|
+
<% [nil, "hover", "pressed", "focus"].each do |state| %>
|
|
56
|
+
<td class="p-1.5"><button type="button" class="btn <%= classes %>" tabindex="-1" <%= %(data-lit-state="#{state}").html_safe if state %>>Go</button></td>
|
|
57
|
+
<% end %>
|
|
58
|
+
<td class="p-1.5"><button type="button" class="btn <%= classes %>" disabled>Go</button></td>
|
|
59
|
+
</tr>
|
|
60
|
+
<% end %>
|
|
61
|
+
</tbody>
|
|
62
|
+
</table>
|
|
63
|
+
<div class="flex flex-wrap items-center gap-2 mt-3">
|
|
64
|
+
<span class="badge text-heading border-subtle">Badge</span>
|
|
65
|
+
<span class="badge" style="color: var(--color-success-ink); border-color: var(--color-success)">Live</span>
|
|
66
|
+
<input class="input-field max-w-[12rem]" type="text" value="Input, pressed in" aria-label="Input specimen" readonly>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
<% end %>
|
|
70
|
+
<%= render layout: "style/specimen",
|
|
71
|
+
locals: { klass: "studio_icon(name, size: 18)",
|
|
72
|
+
usage: %(studio_icon("rocket", size: 22)) } do %>
|
|
73
|
+
<ul class="grid grid-cols-4 gap-3 w-full text-heading" data-test="style-glass-icons">
|
|
74
|
+
<% Studio::Icons.names.each do |name| %>
|
|
75
|
+
<li class="flex flex-col items-center gap-1 min-w-0" title="<%= name %>">
|
|
76
|
+
<%= studio_icon(name, size: 22) %>
|
|
77
|
+
<span class="text-3xs text-muted truncate max-w-full"><%= name %></span>
|
|
78
|
+
</li>
|
|
79
|
+
<% end %>
|
|
80
|
+
</ul>
|
|
81
|
+
<% end %>
|
|
82
|
+
</div>
|
|
83
|
+
</section>
|
|
@@ -93,16 +93,8 @@
|
|
|
93
93
|
<section id="host-modals" class="space-y-8" x-data="{}"
|
|
94
94
|
style="scroll-margin-top: calc(var(--nav-h, 0px) + 5rem)">
|
|
95
95
|
<div class="space-y-1">
|
|
96
|
-
<h2 class="
|
|
97
|
-
<p class="text-muted text-sm max-w-2xl">
|
|
98
|
-
This app’s own modals, contributed by
|
|
99
|
-
<code class="font-mono text-2xs bg-inset px-1.5 py-0.5 rounded">app/views/style/host/_modals.html.erb</code>.
|
|
100
|
-
Every section above belongs to the engine and is inherited; everything
|
|
101
|
-
here belongs to <%= Studio.app_name %> and ships with it. These specimens
|
|
102
|
-
drive the app’s <strong>real</strong>
|
|
103
|
-
<code class="font-mono text-2xs bg-inset px-1.5 py-0.5 rounded">$store.modals</code>
|
|
104
|
-
host, so the card that opens here is the card that opens in production.
|
|
105
|
-
</p>
|
|
96
|
+
<h2 class="type-h3 text-heading"><%= Studio.app_name %></h2>
|
|
97
|
+
<p class="text-muted text-sm max-w-2xl">This app’s own modals, from <code class="font-mono text-2xs">app/views/style/host/_modals.html.erb</code>.</p>
|
|
106
98
|
</div>
|
|
107
99
|
|
|
108
100
|
<%# The app's content. Reached only when it resolves - style/index.html.erb
|