plutonium 0.68.0 → 0.70.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/.claude/skills/plutonium-async-interactions/SKILL.md +41 -2
- data/.claude/skills/plutonium-resource/SKILL.md +13 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +37 -0
- data/CLAUDE.md +11 -0
- data/SECURITY.md +1 -1
- data/app/assets/plutonium.css +3 -1
- data/app/assets/plutonium.png +0 -0
- data/config/locales/en/async.yml +11 -1
- data/config/locales/en/ui.yml +3 -0
- data/docs/blog/introducing-plutonium-dashboards.md +4 -4
- data/docs/blog/the-slowest-query-is-the-count.md +109 -0
- data/docs/guides/performance.md +14 -0
- data/docs/public/favicon-16x16.png +0 -0
- data/docs/public/favicon-32x32.png +0 -0
- data/docs/public/images/blog/i18n-es-admin.png +0 -0
- data/docs/public/images/blog/pagination-capped.png +0 -0
- data/docs/public/images/blog/pagination-countless.png +0 -0
- data/docs/public/images/components/avatar.png +0 -0
- data/docs/public/images/guides/custom-actions-bulk.png +0 -0
- data/docs/public/images/guides/dashboard-card-error.png +0 -0
- data/docs/public/images/guides/dashboard-loading.png +0 -0
- data/docs/public/images/guides/dashboard-overview.png +0 -0
- data/docs/public/images/guides/dashboard-sidebar.png +0 -0
- data/docs/public/images/guides/kanban-after-move.png +0 -0
- data/docs/public/images/guides/kanban-board-light.png +0 -0
- data/docs/public/images/guides/kanban-board.png +0 -0
- data/docs/public/images/guides/kanban-show-centered-modal.png +0 -0
- data/docs/public/images/guides/kanban-wip-toast.png +0 -0
- data/docs/public/images/guides/multi-tenancy-dashboard.png +0 -0
- data/docs/public/images/guides/multi-tenancy-welcome.png +0 -0
- data/docs/public/images/guides/nested-inputs.png +0 -0
- data/docs/public/images/guides/nested-resources-tab.png +0 -0
- data/docs/public/images/guides/search-filtering-index.png +0 -0
- data/docs/public/images/guides/search-filtering-panel.png +0 -0
- data/docs/public/images/guides/theming-after.png +0 -0
- data/docs/public/images/guides/theming-before.png +0 -0
- data/docs/public/images/guides/user-invites-landing.png +0 -0
- data/docs/public/images/guides/user-profile-edit.png +0 -0
- data/docs/public/images/guides/user-profile-show.png +0 -0
- data/docs/public/images/guides/wizards-chooser.png +0 -0
- data/docs/public/images/guides/wizards-completed.png +0 -0
- data/docs/public/images/guides/wizards-index-action.png +0 -0
- data/docs/public/images/guides/wizards-repeater.png +0 -0
- data/docs/public/images/guides/wizards-review.png +0 -0
- data/docs/public/images/guides/wizards-step.png +0 -0
- data/docs/public/images/home/tour-actions.png +0 -0
- data/docs/public/images/home/tour-async.png +0 -0
- data/docs/public/images/home/tour-kanban.png +0 -0
- data/docs/public/images/home/tour-tenancy.png +0 -0
- data/docs/public/images/home/tour-wizard.png +0 -0
- data/docs/public/images/home-index.png +0 -0
- data/docs/public/images/home-new.png +0 -0
- data/docs/public/images/home-show.png +0 -0
- data/docs/public/images/reference/async-progress-page.png +0 -0
- data/docs/public/images/reference/async-running-banner.png +0 -0
- data/docs/public/images/reference/structured-inputs-removed.png +0 -0
- data/docs/public/images/reference/structured-inputs.png +0 -0
- data/docs/public/images/tutorial/02-empty-index.png +0 -0
- data/docs/public/images/tutorial/02-index-with-posts.png +0 -0
- data/docs/public/images/tutorial/02-new-form-modal.png +0 -0
- data/docs/public/images/tutorial/02-new-form.png +0 -0
- data/docs/public/images/tutorial/03-create-account.png +0 -0
- data/docs/public/images/tutorial/03-login.png +0 -0
- data/docs/public/images/tutorial/04-admin-index.png +0 -0
- data/docs/public/images/tutorial/05-actions-menu.png +0 -0
- data/docs/public/images/tutorial/05-row-actions.png +0 -0
- data/docs/public/images/tutorial/06-comments-tab.png +0 -0
- data/docs/public/images/tutorial/06-post-with-comments.png +0 -0
- data/docs/public/images/tutorial/07-author-dashboard.png +0 -0
- data/docs/public/images/tutorial/07-author-portal.png +0 -0
- data/docs/public/images/tutorial/08-customized-index.png +0 -0
- data/docs/public/og-image.png +0 -0
- data/docs/public/plutonium.png +0 -0
- data/docs/public/tutorial/plutonium-association-panel.png +0 -0
- data/docs/public/tutorial/plutonium-dashboard.png +0 -0
- data/docs/public/tutorial/plutonium-login-page.png +0 -0
- data/docs/public/tutorial/plutonium-nested-form.png +0 -0
- data/docs/public/tutorial/plutonium-posts-dashboard-customized.png +0 -0
- data/docs/public/tutorial/plutonium-posts-dashboard.png +0 -0
- data/docs/public/tutorial/plutonium-posts-detail-customized.png +0 -0
- data/docs/public/tutorial/plutonium-posts-detail.png +0 -0
- data/docs/public/tutorial/plutonium-publish-post.png +0 -0
- data/docs/reference/behavior/async-interactions.md +62 -5
- data/docs/reference/configuration.md +1 -0
- data/docs/reference/generators/lite.md +30 -6
- data/docs/reference/i18n.md +2 -2
- data/docs/reference/resource/definition.md +35 -0
- data/docs/reference/ui/layouts.md +13 -0
- data/docs/superpowers/plans/2026-10-15-async-run-pre-check.md +1056 -0
- data/docs/superpowers/plans/2026-10-15-async-run-pre-check.md.tasks.json +49 -0
- data/docs/superpowers/specs/2026-10-15-async-run-pre-check-design.md +208 -0
- data/gemfiles/rails_7.gemfile.lock +5 -5
- data/gemfiles/rails_8.0.gemfile.lock +3 -3
- data/gemfiles/rails_8.1.gemfile.lock +3 -3
- data/lib/generators/pu/lite/maintenance/maintenance_generator.rb +20 -1
- data/lib/generators/pu/lite/maintenance/templates/app/jobs/sqlite_maintenance_job.rb.tt +18 -1
- data/lib/plutonium/configuration.rb +17 -0
- data/lib/plutonium/definition/base.rb +22 -0
- data/lib/plutonium/definition/presentable.rb +4 -1
- data/lib/plutonium/interaction/async/configuration.rb +15 -0
- data/lib/plutonium/interaction/async/context.rb +12 -2
- data/lib/plutonium/interaction/async/executor.rb +54 -0
- data/lib/plutonium/interaction/async/run.rb +128 -14
- data/lib/plutonium/interaction/async/run_definition.rb +5 -4
- data/lib/plutonium/interaction/async/run_policy.rb +5 -5
- data/lib/plutonium/interaction/response/redirect.rb +19 -7
- data/lib/plutonium/pagination.rb +35 -0
- data/lib/plutonium/resource/controllers/crud_actions/index_action.rb +27 -1
- data/lib/plutonium/ui/breadcrumbs.rb +26 -8
- data/lib/plutonium/ui/grid/card.rb +11 -2
- data/lib/plutonium/ui/grid/resource.rb +2 -2
- data/lib/plutonium/ui/interaction/async/run_progress.rb +25 -11
- data/lib/plutonium/ui/interaction/async/running_banner.rb +21 -5
- data/lib/plutonium/ui/layout/base.rb +0 -9
- data/lib/plutonium/ui/layout/icon_rail.rb +42 -4
- data/lib/plutonium/ui/page/index.rb +23 -4
- data/lib/plutonium/ui/table/components/filter_pills.rb +8 -2
- data/lib/plutonium/ui/table/components/pagy_info.rb +30 -10
- data/lib/plutonium/ui/table/resource.rb +2 -2
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- data/plutonium.gemspec +3 -1
- data/src/css/components.css +12 -1
- data/src/css/font_awesome.css +2328 -0
- data/src/css/plutonium.css +1 -0
- metadata +11 -7
- data/docs/public/images/blog/dashboards-card-error.png +0 -0
- data/docs/public/images/blog/dashboards-loading.png +0 -0
- data/docs/public/images/blog/dashboards-overview.png +0 -0
- data/docs/public/images/blog/dashboards-sidebar.png +0 -0
|
@@ -18,6 +18,12 @@ module Plutonium
|
|
|
18
18
|
# render IconRail.new(menu: @menu) do |rail|
|
|
19
19
|
# rail.with_brand { image_tag("logo.svg", class: "w-8 h-8") }
|
|
20
20
|
# end
|
|
21
|
+
#
|
|
22
|
+
# @example Links pinned above the footer, outside the scrolling nav
|
|
23
|
+
# render IconRail.new(
|
|
24
|
+
# menu: @menu,
|
|
25
|
+
# footer_menu: Phlexi::Menu::Builder.new { |m| m.item "Settings", url: settings_path, icon: Phlex::TablerIcons::Settings }
|
|
26
|
+
# )
|
|
21
27
|
class IconRail < Plutonium::UI::Component::Base
|
|
22
28
|
include Phlex::Slotable
|
|
23
29
|
|
|
@@ -28,9 +34,11 @@ module Plutonium
|
|
|
28
34
|
DEFAULT_MAX_DEPTH = 2
|
|
29
35
|
|
|
30
36
|
# @param menu [Phlexi::Menu::Builder, nil] Menu structure (same shape as SidebarMenu)
|
|
37
|
+
# @param footer_menu [Phlexi::Menu::Builder, nil] Links pinned above the footer, outside the scrolling nav
|
|
31
38
|
# @param max_depth [Integer] Maximum rendering depth (depth 2 supports parent+children)
|
|
32
|
-
def initialize(menu: nil, max_depth: DEFAULT_MAX_DEPTH)
|
|
39
|
+
def initialize(menu: nil, footer_menu: nil, max_depth: DEFAULT_MAX_DEPTH)
|
|
33
40
|
@menu = menu
|
|
41
|
+
@footer_menu = footer_menu
|
|
34
42
|
@max_depth = max_depth
|
|
35
43
|
end
|
|
36
44
|
|
|
@@ -46,6 +54,7 @@ module Plutonium
|
|
|
46
54
|
) do
|
|
47
55
|
render_brand_section
|
|
48
56
|
render_nav_section
|
|
57
|
+
render_footer_menu_section
|
|
49
58
|
render_footer_section
|
|
50
59
|
end
|
|
51
60
|
end
|
|
@@ -72,6 +81,16 @@ module Plutonium
|
|
|
72
81
|
end
|
|
73
82
|
end
|
|
74
83
|
|
|
84
|
+
# Kept ahead of the footer, which stays the rail's last section: the
|
|
85
|
+
# pinned rail's styles right-align that section to place the pin button.
|
|
86
|
+
def render_footer_menu_section
|
|
87
|
+
return if @footer_menu.nil? || @footer_menu.items.empty?
|
|
88
|
+
|
|
89
|
+
div(class: "flex flex-col items-center gap-1 px-2 py-2 border-t border-[var(--pu-border)] shrink-0") do
|
|
90
|
+
render_items(@footer_menu.items, 0)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
75
94
|
def render_footer_section
|
|
76
95
|
div(class: "h-14 flex items-center justify-center border-t border-[var(--pu-border)] shrink-0") do
|
|
77
96
|
render_pin_button
|
|
@@ -116,11 +135,12 @@ module Plutonium
|
|
|
116
135
|
def render_leaf_item(item, depth)
|
|
117
136
|
a(
|
|
118
137
|
href: item.url,
|
|
119
|
-
title: item
|
|
120
|
-
**item_link_attributes(item, "icon-rail-leaf #{leaf_classes(item, depth)}", base_aria: {label: item
|
|
138
|
+
title: item_title(item),
|
|
139
|
+
**item_link_attributes(item, "icon-rail-leaf #{leaf_classes(item, depth)}", base_aria: {label: item_title(item)})
|
|
121
140
|
) do
|
|
122
141
|
render_item_icon(item)
|
|
123
142
|
span(class: "icon-rail-label hidden") { item.label }
|
|
143
|
+
render_trailing_badge(item, "icon-rail-label hidden")
|
|
124
144
|
end
|
|
125
145
|
end
|
|
126
146
|
|
|
@@ -165,14 +185,32 @@ module Plutonium
|
|
|
165
185
|
a(
|
|
166
186
|
href: child.url,
|
|
167
187
|
role: "menuitem",
|
|
188
|
+
title: item_title(child),
|
|
168
189
|
**item_link_attributes(child, "icon-rail-flyout-item")
|
|
169
|
-
)
|
|
190
|
+
) do
|
|
191
|
+
plain child.label
|
|
192
|
+
render_trailing_badge(child)
|
|
193
|
+
end
|
|
170
194
|
end
|
|
171
195
|
end
|
|
172
196
|
end
|
|
173
197
|
end
|
|
174
198
|
end
|
|
175
199
|
|
|
200
|
+
# The label, with the item's trailing badge in brackets, for tooltips
|
|
201
|
+
# and screen readers: "Reports (Pro)".
|
|
202
|
+
def item_title(item)
|
|
203
|
+
item.trailing_badge ? "#{item.label} (#{item.trailing_badge})" : item.label
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# A short marker after the label, such as the plan that unlocks a page.
|
|
207
|
+
def render_trailing_badge(item, extra_classes = nil)
|
|
208
|
+
return unless item.trailing_badge
|
|
209
|
+
|
|
210
|
+
span(class: ["icon-rail-badge ml-auto rounded px-1.5 py-0.5 text-[10px] font-medium leading-none " \
|
|
211
|
+
"bg-[var(--pu-surface-alt)] text-[var(--pu-text-muted)]", extra_classes].compact.join(" ")) { item.trailing_badge }
|
|
212
|
+
end
|
|
213
|
+
|
|
176
214
|
def render_item_icon(item)
|
|
177
215
|
if item.icon
|
|
178
216
|
render item.icon.new(class: "w-5 h-5 shrink-0")
|
|
@@ -4,6 +4,11 @@ module Plutonium
|
|
|
4
4
|
module UI
|
|
5
5
|
module Page
|
|
6
6
|
class Index < Base
|
|
7
|
+
# How many recently finished runs the running banner lists at most. It
|
|
8
|
+
# sits above the collection; a burst of dispatches must not push the
|
|
9
|
+
# collection off the screen. The run index has the rest.
|
|
10
|
+
RECENT_RUNS_LIMIT = 5
|
|
11
|
+
|
|
7
12
|
# Cookie name carrying a per-resource view preference. Single
|
|
8
13
|
# source of truth — Table::Resource, Grid::Resource, and the
|
|
9
14
|
# Stimulus view-switcher controller all read from here. Underscored
|
|
@@ -101,12 +106,26 @@ module Plutonium
|
|
|
101
106
|
# a URL for a route that doesn't exist here.
|
|
102
107
|
return unless registered_resources.include?(Plutonium::Interaction::Async::Run)
|
|
103
108
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
relation:
|
|
109
|
+
run_class = Plutonium::Interaction::Async::Run
|
|
110
|
+
in_progress = authorized_resource_scope(
|
|
111
|
+
run_class, relation: run_class.for_target(resource_class).in_progress
|
|
112
|
+
).to_a
|
|
113
|
+
|
|
114
|
+
# The current user's own runs that settled moments ago. A dispatch
|
|
115
|
+
# sends them back here, and a short run (or one its pre_check rejected)
|
|
116
|
+
# finishes before this page renders, so "in progress" alone would show
|
|
117
|
+
# them nothing. Only their own: someone else's finished run is not news.
|
|
118
|
+
recent = authorized_resource_scope(
|
|
119
|
+
run_class,
|
|
120
|
+
relation: run_class.for_target(resource_class)
|
|
121
|
+
.where(initiator: current_user)
|
|
122
|
+
.finished_since(Plutonium.configuration.async_interactions.recent_runs_window.ago)
|
|
123
|
+
.order(finished_at: :desc)
|
|
124
|
+
.limit(RECENT_RUNS_LIMIT)
|
|
107
125
|
).to_a
|
|
108
126
|
|
|
109
|
-
|
|
127
|
+
# uniq: a run that finishes between the two queries is in both.
|
|
128
|
+
render Plutonium::UI::Interaction::Async::RunningBanner.new(runs: (in_progress + recent).uniq)
|
|
110
129
|
end
|
|
111
130
|
|
|
112
131
|
def selected_view
|
|
@@ -5,9 +5,14 @@ module Plutonium
|
|
|
5
5
|
module Table
|
|
6
6
|
module Components
|
|
7
7
|
class FilterPills < Plutonium::UI::Component::Base
|
|
8
|
-
|
|
8
|
+
# @param total_count [Integer, nil] nil hides the count (countless
|
|
9
|
+
# pagination, `pagination count: false`)
|
|
10
|
+
# @param count_capped [Boolean] the count stopped at its cap
|
|
11
|
+
# (`pagination count: 10_000`), so it reads "10000+ results"
|
|
12
|
+
def initialize(query:, total_count: nil, count_capped: false)
|
|
9
13
|
@query = query
|
|
10
14
|
@total_count = total_count
|
|
15
|
+
@count_capped = count_capped
|
|
11
16
|
end
|
|
12
17
|
|
|
13
18
|
def view_template
|
|
@@ -79,7 +84,8 @@ module Plutonium
|
|
|
79
84
|
|
|
80
85
|
def render_result_count
|
|
81
86
|
div(class: "ml-auto text-xs text-[var(--pu-text-muted)]") do
|
|
82
|
-
|
|
87
|
+
key = @count_capped ? "plutonium.ui.table.result_count_capped" : "plutonium.ui.table.result_count"
|
|
88
|
+
plain t(key, count: @total_count)
|
|
83
89
|
end
|
|
84
90
|
end
|
|
85
91
|
end
|
|
@@ -34,21 +34,41 @@ module Plutonium
|
|
|
34
34
|
private
|
|
35
35
|
|
|
36
36
|
def results_info
|
|
37
|
-
key = if @pagy.count.zero?
|
|
38
|
-
"pagy.info_tag.no_items"
|
|
39
|
-
elsif @pagy.in == @pagy.count
|
|
40
|
-
"pagy.info_tag.single_page"
|
|
41
|
-
else
|
|
42
|
-
"pagy.info_tag.multiple_pages"
|
|
43
|
-
end
|
|
44
|
-
|
|
45
37
|
div do
|
|
46
|
-
render_template(
|
|
47
|
-
b { @pagy.public_send(placeholder).to_s }
|
|
38
|
+
render_template(results_template) do |placeholder|
|
|
39
|
+
b { (placeholder == :count) ? count_label : @pagy.public_send(placeholder).to_s }
|
|
48
40
|
end
|
|
49
41
|
end
|
|
50
42
|
end
|
|
51
43
|
|
|
44
|
+
def results_template
|
|
45
|
+
count = known_count
|
|
46
|
+
if count.nil?
|
|
47
|
+
# Countless, past page 1: the total is unknown, so no "of N".
|
|
48
|
+
t("plutonium.ui.table.page_range", item_name: item_name(2), from: "%{from}", to: "%{to}")
|
|
49
|
+
elsif count.zero?
|
|
50
|
+
pagy_template("pagy.info_tag.no_items", count)
|
|
51
|
+
elsif count == @pagy.in
|
|
52
|
+
pagy_template("pagy.info_tag.single_page", count)
|
|
53
|
+
else
|
|
54
|
+
pagy_template("pagy.info_tag.multiple_pages", count)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# The total, when it is known. A countless pager (`pagination count:
|
|
59
|
+
# false`) has none, except on a first page with no next page, where the
|
|
60
|
+
# rows on it are the whole set.
|
|
61
|
+
def known_count
|
|
62
|
+
return @pagy.count if @pagy.count
|
|
63
|
+
@pagy.in if @pagy.page == 1 && !@pagy.next
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# "10000+" when the count hit its cap (`pagination count: 10_000`).
|
|
67
|
+
def count_label
|
|
68
|
+
count = known_count.to_s
|
|
69
|
+
@pagy.options[:count_capped] ? t("plutonium.ui.table.capped_count", count:) : count
|
|
70
|
+
end
|
|
71
|
+
|
|
52
72
|
def per_page_selector
|
|
53
73
|
id = "perPage#{SecureRandom.hex}"
|
|
54
74
|
|
|
@@ -71,7 +71,7 @@ module Plutonium
|
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
def render_filter_pills
|
|
74
|
-
TableFilterPills(query: current_query_object, total_count: pagy_instance&.count)
|
|
74
|
+
TableFilterPills(query: current_query_object, total_count: pagy_instance&.count, count_capped: pagy_instance&.options&.fetch(:count_capped, false))
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
def current_search_url
|
|
@@ -315,7 +315,7 @@ module Plutonium
|
|
|
315
315
|
|
|
316
316
|
def render_footer
|
|
317
317
|
div(class: "lg:sticky bottom-[-2px] mt-1 p-4 pb-6 w-full z-30 bg-[var(--pu-body)]") {
|
|
318
|
-
TableInfo(pagy_instance, item_name: resource_name(resource_class, pagy_instance.count))
|
|
318
|
+
TableInfo(pagy_instance, item_name: resource_name(resource_class, pagy_instance.count || 2))
|
|
319
319
|
TablePagination(pagy_instance)
|
|
320
320
|
}
|
|
321
321
|
end
|
data/lib/plutonium/version.rb
CHANGED
data/package.json
CHANGED
data/plutonium.gemspec
CHANGED
|
@@ -35,7 +35,9 @@ Gem::Specification.new do |spec|
|
|
|
35
35
|
f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile]) ||
|
|
36
36
|
# Maintainer tooling; host apps read tailwind.options.js and the postcss
|
|
37
37
|
# plugin from the gem, not the lockfile or yarn settings.
|
|
38
|
-
%w[.yarnrc.yml yarn.lock public/plutonium-assets/].any? { |prefix| f.start_with?(prefix) }
|
|
38
|
+
%w[.yarnrc.yml yarn.lock public/plutonium-assets/].any? { |prefix| f.start_with?(prefix) } ||
|
|
39
|
+
# Full-size brand sources; the gem ships the resized app/assets copy.
|
|
40
|
+
f.start_with?("brand/")
|
|
39
41
|
end
|
|
40
42
|
end
|
|
41
43
|
spec.bindir = "exe"
|
data/src/css/components.css
CHANGED
|
@@ -918,7 +918,9 @@ html.pu-rail-pinned .icon-rail-pin-expand {
|
|
|
918
918
|
}
|
|
919
919
|
|
|
920
920
|
.icon-rail-flyout-item {
|
|
921
|
-
display:
|
|
921
|
+
display: flex;
|
|
922
|
+
align-items: center;
|
|
923
|
+
gap: 8px;
|
|
922
924
|
padding: 7px 10px;
|
|
923
925
|
font-size: 13px;
|
|
924
926
|
color: var(--pu-text);
|
|
@@ -1222,6 +1224,15 @@ html.pu-rail-pinned .icon-rail-pin-expand {
|
|
|
1222
1224
|
margin-left: var(--pu-drag-marker-gap, 4px);
|
|
1223
1225
|
}
|
|
1224
1226
|
|
|
1227
|
+
/* The failures on an async run's progress panel: an inline alert holding a
|
|
1228
|
+
list, so it sits flush in the card and the list keeps its bullets. */
|
|
1229
|
+
.pu-run-progress-failures {
|
|
1230
|
+
@apply mb-0 items-start;
|
|
1231
|
+
}
|
|
1232
|
+
.pu-run-progress-failures .pu-alert-message {
|
|
1233
|
+
@apply ms-0 ps-4 list-disc space-y-1;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1225
1236
|
/* Indeterminate progress for an async run with no denominator (opaque work).
|
|
1226
1237
|
A short segment sweeping the track: the motion is what says "alive", which a
|
|
1227
1238
|
static bar at any width cannot, and a full-width bar would misread as done. */
|