Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/app/components/shared/components_list.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def components
{name: "Shortcut Key", path: docs_shortcut_key_path},
{name: "Sidebar", path: docs_sidebar_path},
{name: "Skeleton", path: docs_skeleton_path},
{name: "Spinner", path: docs_spinner_path},
{name: "Switch", path: docs_switch_path},
{name: "Table", path: docs_table_path},
{name: "Tabs", path: docs_tabs_path},
Expand Down
4 changes: 4 additions & 0 deletions docs/app/controllers/docs_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,10 @@ def skeleton
render Views::Docs::Skeleton.new
end

def spinner
render Views::Docs::Spinner.new
end

def switch
render Views::Docs::Switch.new
end
Expand Down
1 change: 1 addition & 0 deletions docs/app/lib/site_files.rb
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ class SiteFiles
{title: "Shortcut Key", path: "/docs/shortcut_key", description: "Keyboard shortcut display component."},
{title: "Sidebar", path: "/docs/sidebar", description: "Composable, themeable sidebar component."},
{title: "Skeleton", path: "/docs/skeleton", description: "Placeholder for loading states."},
{title: "Spinner", path: "/docs/spinner", description: "Animated indicator for loading states."},
{title: "Switch", path: "/docs/switch", description: "Toggle control for binary settings."},
{title: "Table", path: "/docs/table", description: "Responsive table component."},
{title: "Tabs", path: "/docs/tabs", description: "Layered tab panels displayed one at a time."},
Expand Down
171 changes: 171 additions & 0 deletions docs/app/views/docs/spinner.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# frozen_string_literal: true

class Views::Docs::Spinner < Views::Base
def view_template
component = "Spinner"

div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do
render Docs::Header.new(title: "Spinner", description: "An indicator that can be used to show a loading state.")

Heading(level: 2) { "Usage" }

render Docs::VisualCodeExample.new(title: "Example", context: self) do
<<~RUBY
Spinner()
RUBY
end

render Docs::VisualCodeExample.new(title: "Size", description: "The size is controlled from the outside with any size utility.", context: self) do
<<~RUBY
div(class: "flex items-center gap-6") do
Spinner(class: "size-3")
Spinner(class: "size-4")
Spinner(class: "size-6")
Spinner(class: "size-8")
end
RUBY
end

render Docs::VisualCodeExample.new(title: "Color", description: "The spinner inherits the current text color.", context: self) do
<<~RUBY
div(class: "flex items-center gap-6") do
Spinner(class: "size-6 text-red-500")
Spinner(class: "size-6 text-green-500")
Spinner(class: "size-6 text-blue-500")
Spinner(class: "size-6 text-muted-foreground")
end
RUBY
end

render Docs::VisualCodeExample.new(title: "Button", description: "The button carries the state, the spinner is decorative.", context: self) do
<<~RUBY
div(class: "flex flex-wrap items-center gap-4") do
Button(disabled: true, aria: {busy: "true"}, class: "gap-2") do
Spinner(label: nil)
plain "Loading..."
end
Button(variant: :outline, disabled: true, aria: {busy: "true"}, class: "gap-2") do
Spinner(label: nil)
plain "Please wait"
end
Button(variant: :secondary, size: :sm, disabled: true, aria: {busy: "true"}, class: "gap-2") do
Spinner(label: nil, class: "size-3")
plain "Processing"
end
end
RUBY
end

render Docs::VisualCodeExample.new(title: "Badge", context: self) do
<<~RUBY
div(class: "flex flex-wrap items-center gap-4") do
Badge(class: "gap-1.5") do
Spinner(label: nil, class: "size-3")
plain "Syncing"
end
Badge(variant: :outline, class: "gap-1.5") do
Spinner(label: nil, class: "size-3")
plain "Updating"
end
Badge(variant: :destructive, class: "gap-1.5") do
Spinner(label: nil, class: "size-3")
plain "Deleting"
end
end
RUBY
end

render Docs::VisualCodeExample.new(title: "Empty", description: "Use a spinner as the media of an empty state to build a loading screen.", context: self) do
<<~RUBY
Empty(class: "w-full") do
EmptyHeader do
EmptyMedia(variant: :icon) do
Spinner()
end
EmptyTitle { "Processing your request" }
EmptyDescription { "Please wait while we process your request. Do not refresh the page." }
end
EmptyContent do
Button(variant: :outline, size: :sm) { "Cancel" }
end
end
RUBY
end

Heading(level: 2) { "Customization" }

Text do
plain "The generator copies "
InlineCode { "spinner.rb" }
plain " into "
InlineCode { "app/components/ruby_ui/spinner/" }
plain ", so the component belongs to your app. RubyUI inlines the SVG because the gem ships no icon dependency — but if your app has one, such as "
InlineLink(href: "https://github.com/AliOsm/phlex-icons", target: "_blank") { "phlex-icons" }
plain ", replace "
InlineCode { "view_template" }
plain " with the icon component and drop the two constants."
end

Codeblock(<<~RUBY, syntax: :ruby)
# app/components/ruby_ui/spinner/spinner.rb
module RubyUI
class Spinner < Base
include PhlexIcons

def view_template
Lucide::LoaderPinwheel(**attrs)
end

# initialize and default_attrs stay as they are;
# ICON_PATH and ICON_ATTRS are no longer needed
end
end
RUBY

Text do
plain "Without an icon gem, swap "
InlineCode { "ICON_PATH" }
plain " for the path data of any other 24x24 stroke icon. An icon drawn with more than one path needs those extra elements added to "
InlineCode { "view_template" }
plain " too."
end

Heading(level: 2) { "Accessibility" }

Text do
plain "A standalone spinner announces itself with "
InlineCode { 'role="status"' }
plain " and "
InlineCode { 'aria-label="Loading"' }
plain ". Pass "
InlineCode { "label:" }
plain " to describe the specific operation."
end

Text do
plain "Inside a button, badge or any other labelled control, pass "
InlineCode { "label: nil" }
plain " to render the spinner as decoration ("
InlineCode { 'aria-hidden="true"' }
plain ") and let the control announce the state with "
InlineCode { 'aria-busy="true"' }
plain ". A labelled spinner would otherwise be read out as part of the control's own name."
end

Codeblock(<<~RUBY, syntax: :ruby)
# announces itself
Spinner(label: "Saving changes")
# => <svg role="status" aria-label="Saving changes" ...>

# decorative, inside a control that announces the state
Spinner(label: nil)
# => <svg aria-hidden="true" ...>
RUBY

render Components::ComponentSetup::Tabs.new(component_name: component)

# components
render Docs::ComponentsTable.new(component_files(component))
end
end
end
1 change: 1 addition & 0 deletions docs/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
get "sidebar/example", to: "docs/sidebar#example", as: :docs_sidebar_example
get "sidebar/inset", to: "docs/sidebar#inset_example", as: :docs_sidebar_inset
get "skeleton", to: "docs#skeleton", as: :docs_skeleton
get "spinner", to: "docs#spinner", as: :docs_spinner
get "switch", to: "docs#switch", as: :docs_switch
get "table", to: "docs#table", as: :docs_table
get "tabs", to: "docs#tabs", as: :docs_tabs
Expand Down
5 changes: 5 additions & 0 deletions docs/public/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,11 @@ This file expands the curated /llms.txt map into a compact reference that can be
- URL: https://rubyui.com/docs/skeleton
- Summary: Placeholder for loading states.

### Spinner

- URL: https://rubyui.com/docs/spinner
- Summary: Animated indicator for loading states.

### Switch

- URL: https://rubyui.com/docs/switch
Expand Down
1 change: 1 addition & 0 deletions docs/public/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ Use the core docs first for installation, theming, dark mode, and customization
- [Shortcut Key](https://rubyui.com/docs/shortcut_key): Keyboard shortcut display component.
- [Sidebar](https://rubyui.com/docs/sidebar): Composable, themeable sidebar component.
- [Skeleton](https://rubyui.com/docs/skeleton): Placeholder for loading states.
- [Spinner](https://rubyui.com/docs/spinner): Animated indicator for loading states.
- [Switch](https://rubyui.com/docs/switch): Toggle control for binary settings.
- [Table](https://rubyui.com/docs/table): Responsive table component.
- [Tabs](https://rubyui.com/docs/tabs): Layered tab panels displayed one at a time.
Expand Down
5 changes: 5 additions & 0 deletions docs/public/sitemap.xml
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,11 @@
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://rubyui.com/docs/spinner</loc>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://rubyui.com/docs/switch</loc>
<changefreq>monthly</changefreq>
Expand Down
46 changes: 46 additions & 0 deletions gem/lib/ruby_ui/spinner/spinner.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# frozen_string_literal: true

module RubyUI
class Spinner < Base
# lucide loader-circle
ICON_PATH = "M21 12a9 9 0 1 1-6.219-8.56"

ICON_ATTRS = {
xmlns: "http://www.w3.org/2000/svg",
viewbox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
stroke_width: "2",
stroke_linecap: "round",
stroke_linejoin: "round"
}.freeze

def initialize(label: "Loading", **attrs)
@label = label
super(**attrs)
end

def view_template
svg(**ICON_ATTRS, **attrs) do |s|
s.path(d: ICON_PATH)
end
end

private

def default_attrs
{
**announcement_attrs,
data: {slot: "spinner"},
class: "size-4 shrink-0 animate-spin"
}
end

# A spinner inside a labelled control is decorative — the control announces busy.
def announcement_attrs
return {aria: {hidden: "true"}} if @label.to_s.strip.empty?

{role: "status", aria: {label: @label}}
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
end
end
end
Loading