diff --git a/.devcontainer/compose.yaml b/.devcontainer/compose.yaml index a86c93f8..614da537 100644 --- a/.devcontainer/compose.yaml +++ b/.devcontainer/compose.yaml @@ -8,5 +8,8 @@ services: volumes: - ../../ruby_ui:/workspaces/ruby_ui:cached + - ../../web:/workspaces/web:cached + ports: + - "3000:3000" # Overrides default command so things don't shut down after the process ends. command: sleep infinity diff --git a/.herb.yml b/.herb.yml new file mode 100644 index 00000000..0b60b906 --- /dev/null +++ b/.herb.yml @@ -0,0 +1,13 @@ +files: + include: + - "lib/**/*.html.erb" + +engine: + validators: + security: true + nesting: true + accessibility: true + +linter: + enabled: true + failLevel: error diff --git a/Gemfile.lock b/Gemfile.lock index 746ab8ad..86caa03b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,6 +7,8 @@ GEM remote: https://rubygems.org/ specs: ast (2.4.2) + herb (0.9.5) + herb (0.9.5-arm64-darwin) json (2.8.0) language_server-protocol (3.17.0.3) lint_roller (1.1.0) @@ -59,6 +61,7 @@ PLATFORMS ruby DEPENDENCIES + herb (~> 0.1) minitest (~> 5.0) phlex (>= 2.1.2) rake (~> 13.0) diff --git a/docs/superpowers/specs/2026-04-09-herb-full-migration-design.md b/docs/superpowers/specs/2026-04-09-herb-full-migration-design.md new file mode 100644 index 00000000..51454cbe --- /dev/null +++ b/docs/superpowers/specs/2026-04-09-herb-full-migration-design.md @@ -0,0 +1,278 @@ +# RubyUI: Full Herb Migration Design + +**Date:** 2026-04-09 +**Branch:** da/herb-experiment +**Status:** Approved + +--- + +## Summary + +Migrate all 44 RubyUI components from Phlex source to Herb (HTML+ERB) as the sole source of truth. Phlex is no longer authored by hand — it is generated on demand by `HerbToPhlexVisitor` when a consumer runs `rails g ruby_ui:component Name --engine=phlex`. + +--- + +## Source Structure (per component) + +``` +lib/ruby_ui// + .rb ← plain Ruby class (no Phlex, uses TailwindMerge) + .html.erb ← Herb template (source of truth) + _docs.html.erb ← ERB usage examples (replaces _docs.rb) + _controller.js ← Stimulus controller (unchanged) +``` + +**What is deleted:** all hand-written Phlex `.rb` files. `base.rb` stays — it is the Phlex base class generated into consumer apps that choose `--engine=phlex`. + +--- + +## Plain Ruby Class (`.rb`) + +No Phlex inheritance. Owns all logic: + +- Accepts `**attrs` for arbitrary HTML attributes +- Runs TailwindMerge to compute final `class` string +- Exposes `attrs` hash (type, class, data-*, id, disabled, etc.) +- Contains all variant/size/state computation as private methods + +```ruby +# frozen_string_literal: true + +require 'tailwind_merge' + +module RubyUI + class Button + TAILWIND_MERGER = ::TailwindMerge::Merger.new.freeze + + def initialize(type: :button, variant: :primary, size: :md, **attrs) + @type = type + @variant = variant.to_sym + user_class = attrs.delete(:class) + @attrs = { type: @type, class: merge_classes(user_class), **attrs } + end + + def attrs = @attrs + + private + + def merge_classes(user_class) + TAILWIND_MERGER.merge([base_classes, variant_classes, user_class].flatten.compact) + end + + # ... variant_classes, base_classes, size_classes ... + end +end +``` + +--- + +## Template (`.html.erb`) + +Uses `tag_attributes` helper to spread the full attrs hash safely: + +```erb + +``` + +For nested structures (Progress, Table, AspectRatio): + +```erb +
+ > + <%= yield %> +
+
+``` + +--- + +## `tag_attributes` Helper + +Shared helper at `lib/ruby_ui/helpers/tag_attributes.rb`. Converts a Ruby hash to an HTML-safe attributes string, handling nested hashes (`data: { controller: 'foo' }` → `data-controller="foo"`). + +Available in all ERB templates and in the plain Ruby class tests. + +--- + +## Generator: `--engine` Flag + +| Engine | Consumer receives | +|--------|-------------------| +| `--engine=phlex` (default) | `HerbToPhlexVisitor` runs on `.html.erb` → writes generated `.rb` Phlex class to `app/components/ruby_ui/` | +| `--engine=erb` | Copies `.rb` + `.html.erb` to `app/components/ruby_ui/` | +| `--engine=herb` | Same files as `--engine=erb` + runs `bundle add herb` in consumer app | + +`--engine=herb` vs `--engine=erb`: identical files, identical content. The only difference is the herb gem install step and the signal to the consumer that Herb::Engine should process the templates. + +**Dependency propagation:** `--engine` flag is passed through to all component dependencies (e.g., AlertDialog depends on Button → both get the same engine). + +--- + +## Docs: `_docs.html.erb` + +Replaces `_docs.rb`. Written in ERB, showing consumer usage: + +```erb +<%= render Button.new(variant: :primary) { 'Primary' } %> +<%= render Button.new(variant: :secondary) { 'Secondary' } %> +<%= render Button.new(variant: :destructive) { 'Destructive' } %> +``` + +The web docs site (`web/`) auto-generates the Phlex tab by running `HerbToPhlexVisitor` on the docs ERB, converting: +- `<%= render Button.new(variant: :primary) { 'Primary' } %>` → `RubyUI.Button(variant: :primary) { 'Primary' }` + +**Web docs tab layout:** +``` +[ Preview ] [ ERB ] [ Phlex ] +``` + +- **Preview** — Rails renders `_docs.html.erb` normally via ActionView +- **ERB** — raw `_docs.html.erb` file content read as a string and displayed as code +- **Phlex** — `HerbToPhlexVisitor` runs on that same raw string → displayed as generated Phlex code + +The `_docs.html.erb` is one file that serves all three purposes: +1. Renderable ERB template (Preview tab) +2. Raw string source (ERB tab) +3. Visitor input (Phlex tab) + +```ruby +# web/ docs controller/helper +erb_source = File.read(component_docs_path) # raw → ERB tab +phlex_code = RubyUI::Herb::PhlexGenerator # → Phlex tab + .generate_view_template(erb_source) +# Rails renders template normally for Preview +``` + +No markdown files, no separate string constants. One file, three uses. + +No manual Phlex docs to maintain. Single source of truth for docs. + +--- + +## HerbToPhlexVisitor Extensions Needed + +The existing visitor handles HTML elements, ERB output tags, yield, conditionals, blocks. New patterns needed for full migration: + +1. **`render Component.new(...)` → `RubyUI.ComponentName(...)`** — for docs conversion and nested component templates +2. **Nested attrs spreading** — `tag_attributes(hash)` in template → Phlex `**attrs` in output +3. **Inline conditionals in attrs** — `class: [@class, condition ? 'a' : 'b']` patterns + +--- + +## Tests + +### Component tests +Convert existing Phlex component tests to test via visitor: +```ruby +def test_button_renders_correct_html + template = File.read('lib/ruby_ui/button/button.html.erb') + phlex_code = PhlexGenerator.generate_view_template(template) + # Eval + render and assert HTML output +end +``` + +### Plain Ruby class tests +Keep `button_herb_test.rb` pattern — test attrs computation, TailwindMerge, variants, sizes directly on the `.rb` class. + +### Generator tests +Keep existing engine tests. Add `--engine=herb` coverage. + +### All tests must pass: `bundle exec rake` green after every batch. + +--- + +## RuboCop / StandardRB + +The gem uses **StandardRB** (double-quoted strings, specific style rules). All new `.rb` files must pass `bundle exec rake standard` before commit. + +--- + +## Component Tiers + +### Tier 1 — Trivial (~32 components) +Single tag + yield. Template is one line. + +Components: Accordion, AccordionContent, AccordionIcon, AccordionItem, AccordionTrigger, Alert, AlertDescription, AlertTitle, AlertDialog (all sub-components), Avatar, AvatarFallback, AvatarImage, Badge, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, BreadcrumbEllipsis, Card (all sub-components), Carousel (all sub-components), Collapsible, CollapsibleContent, CollapsibleTrigger, Command (all sub-components), ContextMenu (all sub-components), Dialog (all sub-components), DropdownMenu (all sub-components), Form (all sub-components), HoverCard (all sub-components), Pagination (all sub-components), Popover (all sub-components), Select (all sub-components), Sheet (all sub-components), ShortcutKey, Skeleton, Tabs (all sub-components), ThemeToggle, Tooltip (all sub-components) + +### Tier 2 — Medium (~8 components) +Multiple elements, computed styles/attrs. + +- **Input** — `` +- **Textarea** — ` diff --git a/lib/ruby_ui/textarea/textarea.rb b/lib/ruby_ui/textarea/textarea.rb index 159395c7..072503ed 100644 --- a/lib/ruby_ui/textarea/textarea.rb +++ b/lib/ruby_ui/textarea/textarea.rb @@ -1,20 +1,19 @@ # frozen_string_literal: true module RubyUI - class Textarea < Base + class Textarea + include ComponentBase + def initialize(rows: 4, **attrs) @rows = rows super(**attrs) end - def view_template(&) - textarea(rows: @rows, **attrs, &) - end - private def default_attrs { + rows: @rows, data: { ruby_ui__form_field_target: "input", action: "input->ruby-ui--form-field#onInput invalid->ruby-ui--form-field#onInvalid" diff --git a/lib/ruby_ui/textarea/textarea_docs.html.erb b/lib/ruby_ui/textarea/textarea_docs.html.erb new file mode 100644 index 00000000..142e4780 --- /dev/null +++ b/lib/ruby_ui/textarea/textarea_docs.html.erb @@ -0,0 +1,5 @@ +<%# Documentation template for %> +
+

+

Use <%= RubyUI:: %> to render the component.

+
diff --git a/lib/ruby_ui/textarea/textarea_docs.rb b/lib/ruby_ui/textarea/textarea_docs.rb deleted file mode 100644 index ce83349a..00000000 --- a/lib/ruby_ui/textarea/textarea_docs.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true - -class Views::Docs::Textarea < Views::Base - def view_template - component = "Textarea" - - div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do - render Docs::Header.new(title: "Textarea", description: "Displays a textarea field.") - - Heading(level: 2) { "Usage" } - - render Docs::VisualCodeExample.new(title: "Textarea", context: self) do - <<~RUBY - div(class: "grid w-full max-w-sm items-center gap-1.5") do - Textarea(placeholder: "Textarea") - end - RUBY - end - - render Docs::VisualCodeExample.new(title: "Disabled", context: self) do - <<~RUBY - div(class: "grid w-full max-w-sm items-center gap-1.5") do - Textarea(disabled: true, placeholder: "Disabled") - end - RUBY - end - - render Docs::VisualCodeExample.new(title: "Aria Disabled", context: self) do - <<~RUBY - div(class: "grid w-full max-w-sm items-center gap-1.5") do - Textarea(aria: {disabled: "true"}, placeholder: "Aria Disabled") - end - RUBY - end - - render Docs::VisualCodeExample.new(title: "With FormField", context: self) do - <<~RUBY - div(class: "grid w-full max-w-sm items-center gap-1.5") do - FormField do - FormFieldLabel(for: "textarea") { "Textarea" } - FormFieldHint { "This is a textarea" } - Textarea(placeholder: "Textarea", id: "textarea") - FormFieldError() - end - end - RUBY - end - end - - render Components::ComponentSetup::Tabs.new(component_name: component) - - render Docs::ComponentsTable.new(component_files(component)) - end -end diff --git a/lib/ruby_ui/textarea/textarea_phlex.rb b/lib/ruby_ui/textarea/textarea_phlex.rb new file mode 100644 index 00000000..9a9396e2 --- /dev/null +++ b/lib/ruby_ui/textarea/textarea_phlex.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module RubyUI + class Textarea < Base + def initialize(rows: 4, **attrs) + @rows = rows + super(**attrs) + end + + def view_template(&) + textarea(**attrs, &) + end + + private + + def default_attrs + { + rows: @rows, + data: { + ruby_ui__form_field_target: "input", + action: "input->ruby-ui--form-field#onInput invalid->ruby-ui--form-field#onInvalid" + }, + class: [ + "flex w-full rounded-md border bg-background px-3 py-1 text-sm shadow-sm transition-colors border-border", + "placeholder:text-muted-foreground", + "disabled:cursor-not-allowed disabled:opacity-50", + "file:border-0 file:bg-transparent file:text-sm file:font-medium", + "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", + "aria-disabled:cursor-not-allowed aria-disabled:opacity-50 aria-disabled:pointer-events-none" + ] + } + end + end +end diff --git a/lib/ruby_ui/theme_toggle/set_dark_mode.html.erb b/lib/ruby_ui/theme_toggle/set_dark_mode.html.erb new file mode 100644 index 00000000..e1ded133 --- /dev/null +++ b/lib/ruby_ui/theme_toggle/set_dark_mode.html.erb @@ -0,0 +1 @@ +
><%= yield %>
diff --git a/lib/ruby_ui/theme_toggle/set_dark_mode.rb b/lib/ruby_ui/theme_toggle/set_dark_mode.rb index 3b307651..c5a92e5f 100644 --- a/lib/ruby_ui/theme_toggle/set_dark_mode.rb +++ b/lib/ruby_ui/theme_toggle/set_dark_mode.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true module RubyUI - class SetDarkMode < Base - def view_template(&) - div(**attrs, &) - end + class SetDarkMode + include ComponentBase + + private def default_attrs { diff --git a/lib/ruby_ui/theme_toggle/set_dark_mode_phlex.rb b/lib/ruby_ui/theme_toggle/set_dark_mode_phlex.rb new file mode 100644 index 00000000..ed528653 --- /dev/null +++ b/lib/ruby_ui/theme_toggle/set_dark_mode_phlex.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module RubyUI + class SetDarkMode < Base + def view_template(&) + div(**attrs, &) + end + + private + + def default_attrs + { + class: "hidden dark:inline-block", + data: {controller: "ruby-ui--theme-toggle", action: "click->ruby-ui--theme-toggle#setLightTheme"} + } + end + end +end diff --git a/lib/ruby_ui/theme_toggle/set_light_mode.html.erb b/lib/ruby_ui/theme_toggle/set_light_mode.html.erb new file mode 100644 index 00000000..e1ded133 --- /dev/null +++ b/lib/ruby_ui/theme_toggle/set_light_mode.html.erb @@ -0,0 +1 @@ +
><%= yield %>
diff --git a/lib/ruby_ui/theme_toggle/set_light_mode.rb b/lib/ruby_ui/theme_toggle/set_light_mode.rb index 00f88fbf..c479f6ab 100644 --- a/lib/ruby_ui/theme_toggle/set_light_mode.rb +++ b/lib/ruby_ui/theme_toggle/set_light_mode.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true module RubyUI - class SetLightMode < Base - def view_template(&) - div(**attrs, &) - end + class SetLightMode + include ComponentBase + + private def default_attrs { diff --git a/lib/ruby_ui/theme_toggle/set_light_mode_phlex.rb b/lib/ruby_ui/theme_toggle/set_light_mode_phlex.rb new file mode 100644 index 00000000..8de93652 --- /dev/null +++ b/lib/ruby_ui/theme_toggle/set_light_mode_phlex.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module RubyUI + class SetLightMode < Base + def view_template(&) + div(**attrs, &) + end + + private + + def default_attrs + { + class: "dark:hidden", + data: {controller: "ruby-ui--theme-toggle", action: "click->ruby-ui--theme-toggle#setDarkTheme"} + } + end + end +end diff --git a/lib/ruby_ui/theme_toggle/theme_toggle.html.erb b/lib/ruby_ui/theme_toggle/theme_toggle.html.erb new file mode 100644 index 00000000..e1ded133 --- /dev/null +++ b/lib/ruby_ui/theme_toggle/theme_toggle.html.erb @@ -0,0 +1 @@ +
><%= yield %>
diff --git a/lib/ruby_ui/theme_toggle/theme_toggle.rb b/lib/ruby_ui/theme_toggle/theme_toggle.rb index 000f8054..1a65eaa2 100644 --- a/lib/ruby_ui/theme_toggle/theme_toggle.rb +++ b/lib/ruby_ui/theme_toggle/theme_toggle.rb @@ -1,9 +1,13 @@ # frozen_string_literal: true module RubyUI - class ThemeToggle < Base - def view_template(&) - div(**attrs, &) + class ThemeToggle + include ComponentBase + + private + + def default_attrs + {} end end end diff --git a/lib/ruby_ui/theme_toggle/theme_toggle_docs.html.erb b/lib/ruby_ui/theme_toggle/theme_toggle_docs.html.erb new file mode 100644 index 00000000..142e4780 --- /dev/null +++ b/lib/ruby_ui/theme_toggle/theme_toggle_docs.html.erb @@ -0,0 +1,5 @@ +<%# Documentation template for %> +
+

+

Use <%= RubyUI:: %> to render the component.

+
diff --git a/lib/ruby_ui/theme_toggle/theme_toggle_docs.rb b/lib/ruby_ui/theme_toggle/theme_toggle_docs.rb deleted file mode 100644 index 1740a924..00000000 --- a/lib/ruby_ui/theme_toggle/theme_toggle_docs.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -class Views::Docs::ThemeToggle < Views::Base - def view_template - component = "ThemeToggle" - - div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do - render Docs::Header.new(title: "Theme Toggle", description: "Toggle between dark/light theme.") - - Heading(level: 2) { "Usage" } - - render Docs::VisualCodeExample.new(title: "With icon", context: self) do - <<~RUBY - ThemeToggle do |toggle| - SetLightMode do - Button(variant: :ghost, icon: true) do - svg( - xmlns: "http://www.w3.org/2000/svg", - viewbox: "0 0 24 24", - fill: "currentColor", - class: "w-4 h-4" - ) do |s| - s.path( - d: - "M12 2.25a.75.75 0 01.75.75v2.25a.75.75 0 01-1.5 0V3a.75.75 0 01.75-.75zM7.5 12a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM18.894 6.166a.75.75 0 00-1.06-1.06l-1.591 1.59a.75.75 0 101.06 1.061l1.591-1.59zM21.75 12a.75.75 0 01-.75.75h-2.25a.75.75 0 010-1.5H21a.75.75 0 01.75.75zM17.834 18.894a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 10-1.061 1.06l1.59 1.591zM12 18a.75.75 0 01.75.75V21a.75.75 0 01-1.5 0v-2.25A.75.75 0 0112 18zM7.758 17.303a.75.75 0 00-1.061-1.06l-1.591 1.59a.75.75 0 001.06 1.061l1.591-1.59zM6 12a.75.75 0 01-.75.75H3a.75.75 0 010-1.5h2.25A.75.75 0 016 12zM6.697 7.757a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 00-1.061 1.06l1.59 1.591z" - ) - end - end - end - - SetDarkMode do - Button(variant: :ghost, icon: true) do - svg( - xmlns: "http://www.w3.org/2000/svg", - viewbox: "0 0 24 24", - fill: "currentColor", - class: "w-4 h-4" - ) do |s| - s.path( - fill_rule: "evenodd", - d: - "M9.528 1.718a.75.75 0 01.162.819A8.97 8.97 0 009 6a9 9 0 009 9 8.97 8.97 0 003.463-.69.75.75 0 01.981.98 10.503 10.503 0 01-9.694 6.46c-5.799 0-10.5-4.701-10.5-10.5 0-4.368 2.667-8.112 6.46-9.694a.75.75 0 01.818.162z", - clip_rule: "evenodd" - ) - end - end - end - end - RUBY - end - - render Docs::VisualCodeExample.new(title: "With text", context: self) do - <<~RUBY - ThemeToggle do |toggle| - SetLightMode do - Button(variant: :primary) { "Light" } - end - - SetDarkMode do - Button(variant: :primary) { "Dark" } - end - end - RUBY - end - - render Components::ComponentSetup::Tabs.new(component_name: component) - - render Docs::ComponentsTable.new(component_files(component)) - end - end -end diff --git a/lib/ruby_ui/theme_toggle/theme_toggle_phlex.rb b/lib/ruby_ui/theme_toggle/theme_toggle_phlex.rb new file mode 100644 index 00000000..03b1fcf6 --- /dev/null +++ b/lib/ruby_ui/theme_toggle/theme_toggle_phlex.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module RubyUI + class ThemeToggle < Base + def view_template(&) + div(**attrs, &) + end + + private + + def default_attrs + {} + end + end +end diff --git a/lib/ruby_ui/tooltip/tooltip.html.erb b/lib/ruby_ui/tooltip/tooltip.html.erb new file mode 100644 index 00000000..e1ded133 --- /dev/null +++ b/lib/ruby_ui/tooltip/tooltip.html.erb @@ -0,0 +1 @@ +
><%= yield %>
diff --git a/lib/ruby_ui/tooltip/tooltip.rb b/lib/ruby_ui/tooltip/tooltip.rb index 70b40dc3..1c8f6d8a 100644 --- a/lib/ruby_ui/tooltip/tooltip.rb +++ b/lib/ruby_ui/tooltip/tooltip.rb @@ -1,16 +1,14 @@ # frozen_string_literal: true module RubyUI - class Tooltip < Base + class Tooltip + include ComponentBase + def initialize(placement: "top", **attrs) @placement = placement super(**attrs) end - def view_template(&) - div(**attrs, &) - end - private def default_attrs diff --git a/lib/ruby_ui/tooltip/tooltip_content.html.erb b/lib/ruby_ui/tooltip/tooltip_content.html.erb new file mode 100644 index 00000000..e1ded133 --- /dev/null +++ b/lib/ruby_ui/tooltip/tooltip_content.html.erb @@ -0,0 +1 @@ +
><%= yield %>
diff --git a/lib/ruby_ui/tooltip/tooltip_content.rb b/lib/ruby_ui/tooltip/tooltip_content.rb index ddeae4b0..74d26cb5 100644 --- a/lib/ruby_ui/tooltip/tooltip_content.rb +++ b/lib/ruby_ui/tooltip/tooltip_content.rb @@ -1,16 +1,14 @@ # frozen_string_literal: true module RubyUI - class TooltipContent < Base + class TooltipContent + include ComponentBase + def initialize(**attrs) @id = "tooltip#{SecureRandom.hex(4)}" super end - def view_template(&) - div(**attrs, &) - end - private def default_attrs diff --git a/lib/ruby_ui/tooltip/tooltip_content_phlex.rb b/lib/ruby_ui/tooltip/tooltip_content_phlex.rb new file mode 100644 index 00000000..ddeae4b0 --- /dev/null +++ b/lib/ruby_ui/tooltip/tooltip_content_phlex.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module RubyUI + class TooltipContent < Base + def initialize(**attrs) + @id = "tooltip#{SecureRandom.hex(4)}" + super + end + + def view_template(&) + div(**attrs, &) + end + + private + + def default_attrs + { + id: @id, + data: { + ruby_ui__tooltip_target: "content" + }, + class: "invisible peer-hover:visible peer-focus:visible w-max absolute top-0 left-0 z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md peer-focus:zoom-in-95 animate-out fade-out-0 zoom-out-95 peer-hover:animate-in peer-focus:animate-in peer-hover:fade-in-0 peer-focus:fade-in-0 peer-hover:zoom-in-95 group-data-[ruby-ui--tooltip-placement-value=bottom]:slide-in-from-top-2 group-data-[ruby-ui--tooltip-placement-value=left]:slide-in-from-right-2 group-data-[ruby-ui--tooltip-placement-value=right]:slide-in-from-left-2 group-data-[ruby-ui--tooltip-placement-value=top]:slide-in-from-bottom-2 delay-500" + } + end + end +end diff --git a/lib/ruby_ui/tooltip/tooltip_docs.html.erb b/lib/ruby_ui/tooltip/tooltip_docs.html.erb new file mode 100644 index 00000000..142e4780 --- /dev/null +++ b/lib/ruby_ui/tooltip/tooltip_docs.html.erb @@ -0,0 +1,5 @@ +<%# Documentation template for %> +
+

+

Use <%= RubyUI:: %> to render the component.

+
diff --git a/lib/ruby_ui/tooltip/tooltip_docs.rb b/lib/ruby_ui/tooltip/tooltip_docs.rb deleted file mode 100644 index 5189728b..00000000 --- a/lib/ruby_ui/tooltip/tooltip_docs.rb +++ /dev/null @@ -1,52 +0,0 @@ -# frozen_string_literal: true - -class Views::Docs::Tooltip < Views::Base - def view_template - component = "Tooltip" - - div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do - render Docs::Header.new(title: "Tooltip", description: "A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it.") - - Heading(level: 2) { "Usage" } - - render Docs::VisualCodeExample.new(title: "Example", context: self) do - <<~RUBY - Tooltip do - TooltipTrigger do - Button(variant: :outline, icon: true) do - bookmark_icon - end - end - TooltipContent do - Text { "Add to library" } - end - end - RUBY - end - - render Components::ComponentSetup::Tabs.new(component_name: component) - - render Docs::ComponentsTable.new(component_files(component)) - end - end - - private - - def bookmark_icon - svg( - xmlns: "http://www.w3.org/2000/svg", - fill: "none", - viewbox: "0 0 24 24", - stroke_width: "1.5", - stroke: "currentColor", - class: "w-4 h-4" - ) do |s| - s.path( - stroke_linecap: "round", - stroke_linejoin: "round", - d: - "M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0111.186 0z" - ) - end - end -end diff --git a/lib/ruby_ui/tooltip/tooltip_phlex.rb b/lib/ruby_ui/tooltip/tooltip_phlex.rb new file mode 100644 index 00000000..70b40dc3 --- /dev/null +++ b/lib/ruby_ui/tooltip/tooltip_phlex.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module RubyUI + class Tooltip < Base + def initialize(placement: "top", **attrs) + @placement = placement + super(**attrs) + end + + def view_template(&) + div(**attrs, &) + end + + private + + def default_attrs + { + data: { + controller: "ruby-ui--tooltip", + ruby_ui__tooltip_placement_value: @placement + }, + class: "group" + } + end + end +end diff --git a/lib/ruby_ui/tooltip/tooltip_trigger.html.erb b/lib/ruby_ui/tooltip/tooltip_trigger.html.erb new file mode 100644 index 00000000..e1ded133 --- /dev/null +++ b/lib/ruby_ui/tooltip/tooltip_trigger.html.erb @@ -0,0 +1 @@ +
><%= yield %>
diff --git a/lib/ruby_ui/tooltip/tooltip_trigger.rb b/lib/ruby_ui/tooltip/tooltip_trigger.rb index a535e942..2b39df21 100644 --- a/lib/ruby_ui/tooltip/tooltip_trigger.rb +++ b/lib/ruby_ui/tooltip/tooltip_trigger.rb @@ -1,10 +1,8 @@ # frozen_string_literal: true module RubyUI - class TooltipTrigger < Base - def view_template(&) - div(**attrs, &) - end + class TooltipTrigger + include ComponentBase private diff --git a/lib/ruby_ui/tooltip/tooltip_trigger_phlex.rb b/lib/ruby_ui/tooltip/tooltip_trigger_phlex.rb new file mode 100644 index 00000000..a535e942 --- /dev/null +++ b/lib/ruby_ui/tooltip/tooltip_trigger_phlex.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module RubyUI + class TooltipTrigger < Base + def view_template(&) + div(**attrs, &) + end + + private + + def default_attrs + { + data: {ruby_ui__tooltip_target: "trigger"}, + variant: :outline, + class: "peer" + } + end + end +end diff --git a/lib/ruby_ui/typography/heading.html.erb b/lib/ruby_ui/typography/heading.html.erb new file mode 100644 index 00000000..9c7f6625 --- /dev/null +++ b/lib/ruby_ui/typography/heading.html.erb @@ -0,0 +1 @@ +<<%= tag_name %> <%= tag_attributes(attrs) %>><%= yield %>> diff --git a/lib/ruby_ui/typography/heading.rb b/lib/ruby_ui/typography/heading.rb index bd883c0f..681aad16 100644 --- a/lib/ruby_ui/typography/heading.rb +++ b/lib/ruby_ui/typography/heading.rb @@ -1,7 +1,9 @@ # frozen_string_literal: true module RubyUI - class Heading < Base + class Heading + include ComponentBase + def initialize(level: nil, as: nil, size: nil, **attrs) @level = level @as = as @@ -9,23 +11,16 @@ def initialize(level: nil, as: nil, size: nil, **attrs) super(**attrs) end - def view_template(&) - tag = determine_tag - public_send(tag, **attrs, &) - end - - private - - def determine_tag + def tag_name return @as if @as return "h#{@level}" if @level "h1" end + private + def default_attrs - { - class: class_names - } + {class: class_names} end def class_names diff --git a/lib/ruby_ui/typography/heading_phlex.rb b/lib/ruby_ui/typography/heading_phlex.rb new file mode 100644 index 00000000..b0acda7f --- /dev/null +++ b/lib/ruby_ui/typography/heading_phlex.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module RubyUI + class Heading < Base + def initialize(level: nil, as: nil, size: nil, **attrs) + @level = level + @as = as + @size = size + super(**attrs) + end + + def tag_name + return @as if @as + return "h#{@level}" if @level + "h1" + end + + def view_template(&) + public_send(tag_name.to_sym, **attrs, &) + end + + private + + def default_attrs + {class: class_names} + end + + def class_names + base_classes = "scroll-m-20 font-bold tracking-tight" + size_class = size_to_class[(@size || level_to_size[@level&.to_s] || "6").to_s] + "#{base_classes} #{size_class}" + end + + def size_to_class + { + "1" => "text-xs", + "2" => "text-sm", + "3" => "text-base", + "4" => "text-lg", + "5" => "text-xl", + "6" => "text-2xl", + "7" => "text-3xl lg:text-4xl", + "8" => "text-4xl", + "9" => "text-5xl" + } + end + + def level_to_size + { + "1" => "7", + "2" => "6", + "3" => "5", + "4" => "4" + } + end + end +end diff --git a/lib/ruby_ui/typography/inline_code.html.erb b/lib/ruby_ui/typography/inline_code.html.erb new file mode 100644 index 00000000..3cdb4ac3 --- /dev/null +++ b/lib/ruby_ui/typography/inline_code.html.erb @@ -0,0 +1 @@ +><%= yield %> diff --git a/lib/ruby_ui/typography/inline_code.rb b/lib/ruby_ui/typography/inline_code.rb index 539c9617..1ccdc097 100644 --- a/lib/ruby_ui/typography/inline_code.rb +++ b/lib/ruby_ui/typography/inline_code.rb @@ -1,17 +1,13 @@ # frozen_string_literal: true module RubyUI - class InlineCode < Base - def view_template(&) - code(**attrs, &) - end + class InlineCode + include ComponentBase private def default_attrs - { - class: "relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold" - } + {class: "relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold"} end end end diff --git a/lib/ruby_ui/typography/inline_code_phlex.rb b/lib/ruby_ui/typography/inline_code_phlex.rb new file mode 100644 index 00000000..3c4e5557 --- /dev/null +++ b/lib/ruby_ui/typography/inline_code_phlex.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module RubyUI + class InlineCode < Base + def view_template(&) + code(**attrs, &) + end + + private + + def default_attrs + {class: "relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold"} + end + end +end diff --git a/lib/ruby_ui/typography/inline_link.html.erb b/lib/ruby_ui/typography/inline_link.html.erb new file mode 100644 index 00000000..ffb8f0b0 --- /dev/null +++ b/lib/ruby_ui/typography/inline_link.html.erb @@ -0,0 +1 @@ +><%= yield %> diff --git a/lib/ruby_ui/typography/inline_link.rb b/lib/ruby_ui/typography/inline_link.rb index 07929dd1..450218e7 100644 --- a/lib/ruby_ui/typography/inline_link.rb +++ b/lib/ruby_ui/typography/inline_link.rb @@ -1,22 +1,18 @@ # frozen_string_literal: true module RubyUI - class InlineLink < Base + class InlineLink + include ComponentBase + def initialize(href:, **attrs) - super(**attrs) @href = href - end - - def view_template(&) - a(href: @href, **attrs, &) + super(**attrs) end private def default_attrs - { - class: "text-primary font-medium hover:underline underline-offset-4 cursor-pointer" - } + {href: @href, class: "text-primary font-medium hover:underline underline-offset-4 cursor-pointer"} end end end diff --git a/lib/ruby_ui/typography/inline_link_phlex.rb b/lib/ruby_ui/typography/inline_link_phlex.rb new file mode 100644 index 00000000..c4e3595d --- /dev/null +++ b/lib/ruby_ui/typography/inline_link_phlex.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module RubyUI + class InlineLink < Base + def initialize(href:, **attrs) + @href = href + super(**attrs) + end + + def view_template(&) + a(**attrs, &) + end + + private + + def default_attrs + {href: @href, class: "text-primary font-medium hover:underline underline-offset-4 cursor-pointer"} + end + end +end diff --git a/lib/ruby_ui/typography/text.html.erb b/lib/ruby_ui/typography/text.html.erb new file mode 100644 index 00000000..9c7f6625 --- /dev/null +++ b/lib/ruby_ui/typography/text.html.erb @@ -0,0 +1 @@ +<<%= tag_name %> <%= tag_attributes(attrs) %>><%= yield %>> diff --git a/lib/ruby_ui/typography/text.rb b/lib/ruby_ui/typography/text.rb index b3945936..21219671 100644 --- a/lib/ruby_ui/typography/text.rb +++ b/lib/ruby_ui/typography/text.rb @@ -1,7 +1,9 @@ # frozen_string_literal: true module RubyUI - class Text < Base + class Text + include ComponentBase + def initialize(as: "p", size: "3", weight: "regular", **attrs) @as = as @size = size @@ -9,16 +11,14 @@ def initialize(as: "p", size: "3", weight: "regular", **attrs) super(**attrs) end - def view_template(&) - public_send(@as, **attrs, &) + def tag_name + @as end private def default_attrs - { - class: class_names - } + {class: class_names} end def class_names diff --git a/lib/ruby_ui/typography/text_phlex.rb b/lib/ruby_ui/typography/text_phlex.rb new file mode 100644 index 00000000..eae141fb --- /dev/null +++ b/lib/ruby_ui/typography/text_phlex.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module RubyUI + class Text < Base + def initialize(as: "p", size: "3", weight: "regular", **attrs) + @as = as + @size = size + @weight = weight + super(**attrs) + end + + def tag_name + @as + end + + def view_template(&) + public_send(tag_name.to_sym, **attrs, &) + end + + private + + def default_attrs + {class: class_names} + end + + def class_names + "#{size_to_class[@size]} #{weight_to_class[@weight]}" + end + + def size_to_class + { + "1" => "text-xs", "xs" => "text-xs", + "2" => "text-sm", "sm" => "text-sm", + "3" => "text-base", "base" => "text-base", + "4" => "text-lg", "lg" => "text-lg", + "5" => "text-xl", "xl" => "text-xl", + "6" => "text-2xl", "2xl" => "text-2xl", + "7" => "text-3xl", "3xl" => "text-3xl", + "8" => "text-4xl", "4xl" => "text-4xl", + "9" => "text-5xl", "5xl" => "text-5xl" + } + end + + def weight_to_class + { + "muted" => "text-muted-foreground", + "light" => "font-light", + "regular" => "font-normal", + "medium" => "font-medium", + "semibold" => "font-semibold", + "bold" => "font-bold" + } + end + end +end diff --git a/lib/ruby_ui/typography/typography_blockquote.html.erb b/lib/ruby_ui/typography/typography_blockquote.html.erb new file mode 100644 index 00000000..07e94327 --- /dev/null +++ b/lib/ruby_ui/typography/typography_blockquote.html.erb @@ -0,0 +1 @@ +
><%= yield %>
diff --git a/lib/ruby_ui/typography/typography_blockquote.rb b/lib/ruby_ui/typography/typography_blockquote.rb index b4d4e843..1dd26b0b 100644 --- a/lib/ruby_ui/typography/typography_blockquote.rb +++ b/lib/ruby_ui/typography/typography_blockquote.rb @@ -1,17 +1,13 @@ # frozen_string_literal: true module RubyUI - class TypographyBlockquote < Base - def view_template(&) - blockquote(**attrs, &) - end + class TypographyBlockquote + include ComponentBase private def default_attrs - { - class: "mt-6 border-l-2 pl-6 italic" - } + {class: "mt-6 border-l-2 pl-6 italic"} end end end diff --git a/lib/ruby_ui/typography/typography_blockquote_phlex.rb b/lib/ruby_ui/typography/typography_blockquote_phlex.rb new file mode 100644 index 00000000..8fa0110e --- /dev/null +++ b/lib/ruby_ui/typography/typography_blockquote_phlex.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module RubyUI + class TypographyBlockquote < Base + def view_template(&) + blockquote(**attrs, &) + end + + private + + def default_attrs + {class: "mt-6 border-l-2 pl-6 italic"} + end + end +end diff --git a/ruby_ui.gemspec b/ruby_ui.gemspec index d921e74f..01237fcc 100644 --- a/ruby_ui.gemspec +++ b/ruby_ui.gemspec @@ -21,4 +21,5 @@ Gem::Specification.new do |s| s.add_development_dependency "rake", "~> 13.0" s.add_development_dependency "standard", "~> 1.0" s.add_development_dependency "minitest", "~> 5.0" + s.add_development_dependency "herb", "~> 0.1" end diff --git a/test/generators/component_generator_engine_test.rb b/test/generators/component_generator_engine_test.rb new file mode 100644 index 00000000..bbc95889 --- /dev/null +++ b/test/generators/component_generator_engine_test.rb @@ -0,0 +1,283 @@ +# frozen_string_literal: true + +require "test_helper" +require "fileutils" +require "tmpdir" +require_relative "../../lib/generators/ruby_ui/engine_utils" +require "ruby_ui/herb/phlex_generator" + +# Tests the --engine flag logic for ComponentGenerator. +# +# New convention (post-full-migration): +# component.rb — plain Ruby class (ComponentBase, no Phlex) +# component.html.erb — Herb template (source of truth) +# component_docs.html.erb — ERB docs (replaces _docs.rb) +# +# Engines: +# --engine=phlex (default): copies .rb files only (excludes .html.erb, _docs files) +# --engine=erb: copies .rb files + .html.erb templates +# --engine=herb: same files as erb (signals consumer to install herb gem) +class ComponentGeneratorEngineTest < Minitest::Test + EngineUtils = RubyUI::Generators::EngineUtils + + # ── Herb template detection ────────────────────────────────── + + def test_detects_herb_template_when_html_erb_present + Dir.mktmpdir do |tmpdir| + FileUtils.touch(File.join(tmpdir, "button.rb")) + File.write(File.join(tmpdir, "button.html.erb"), "") + + assert EngineUtils.herb_component?(tmpdir) + end + end + + def test_no_herb_template_for_rb_only_component + Dir.mktmpdir do |tmpdir| + FileUtils.touch(File.join(tmpdir, "legacy.rb")) + + refute EngineUtils.herb_component?(tmpdir) + end + end + + def test_herb_template_paths_returns_html_erb_files + Dir.mktmpdir do |tmpdir| + File.write(File.join(tmpdir, "button.html.erb"), "') + assert_includes phlex, "class: computed_classes" + refute_includes phlex, "<%=" + end + + def test_multiple_erb_attributes + phlex = generate('') + assert_includes phlex, "type: @type" + assert_includes phlex, "class: classes" + end + + # ── Yield / block tests ─────────────────────────────────────── + + def test_yield_generates_block_param + phlex = generate("") + # Should use the &block shorthand + assert_match(/button.*&/, phlex) + refute_includes phlex, "do\n" + end + + def test_yield_with_attrs_generates_block_param + phlex = generate('') + assert_match(/button.*&/, phlex) + assert_includes phlex, 'type: "submit"' + end + + # ── ERB content tests ───────────────────────────────────────── + + def test_erb_output_tag + phlex = generate("

<%= @name %>

") + assert_includes phlex, "@name" + end + + def test_erb_comment_becomes_ruby_comment + phlex = generate("<%# This is a comment %>
X
") + assert_includes phlex, "# This is a comment" + end + + # ── Nested elements ─────────────────────────────────────────── + + def test_nested_elements + phlex = generate('
Text
') + assert_includes phlex, "div" + assert_includes phlex, "span" + assert_includes phlex, 'plain "Text"' + end + + # ── The actual Button template ──────────────────────────────── + + def test_button_template + template = File.read("lib/ruby_ui/button/button.html.erb") + phlex = generate(template) + + # Should produce a button tag call with splat attrs + assert_includes phlex, "button(" + assert_includes phlex, "**attrs" + assert_match(/&/, phlex) + end + + # ── PhlexGenerator integration ──────────────────────────────── + + def test_phlex_generator_view_template + template = '

<%= @title %>

' + body = RubyUI::Herb::PhlexGenerator.generate_view_template(template) + + assert_includes body, "section" + assert_includes body, "h1" + assert_includes body, "@title" + end + + def test_phlex_generator_class + template = "
<%= yield %>
" + code = RubyUI::Herb::PhlexGenerator.generate_class( + template_source: template, + class_name: "Card", + module_name: "RubyUI" + ) + + assert_includes code, "module RubyUI" + assert_includes code, "class Card < Base" + assert_includes code, "def view_template(&)" + assert_includes code, "div" + end + + def test_phlex_generator_raises_on_parse_error + # Herb may not raise on malformed HTML (it's lenient), but if it does + # report errors, the generator should raise. + # This test documents the expected behavior. + result = ::Herb.parse("
") + if result.errors.any? + assert_raises(ArgumentError) do + RubyUI::Herb::PhlexGenerator.generate_view_template("
") + end + else + # Herb is lenient — just verify it doesn't crash + body = RubyUI::Herb::PhlexGenerator.generate_view_template("
") + assert_kind_of String, body + end + end + + # ── HTML Output Parity ──────────────────────────────────────── + # Verify the generated Phlex code conceptually matches the hand-written + # Button's view_template structure. + + def test_generated_button_matches_handwritten_structure + template = File.read("lib/ruby_ui/button/button.html.erb") + generated_body = generate(template) + + # Template uses tag_attributes(attrs) splat → visitor generates button(**attrs, &) + assert_includes generated_body, "button(" + assert_includes generated_body, "**attrs" + assert_match(/&\)?$/, generated_body.strip) + end + + private + + def generate(source) + result = ::Herb.parse(source) + visitor = RubyUI::Herb::HerbToPhlexVisitor.new + result.visit(visitor) + visitor.to_phlex + end +end diff --git a/test/ruby_ui/hover_card_test.rb b/test/ruby_ui/hover_card_test.rb index 4a578820..3f9ce818 100644 --- a/test/ruby_ui/hover_card_test.rb +++ b/test/ruby_ui/hover_card_test.rb @@ -2,24 +2,52 @@ require "test_helper" -class RubyUI::HoverCardTest < ComponentTest - def test_render_with_all_items - output = phlex do - RubyUI.HoverCard do - RubyUI.HoverCardTrigger do - RubyUI.Button(variant: :link) { "@joeldrapper" } - end - RubyUI.HoverCardContent do |card_content| - card_content.div(class: "flex justify-between space-x-4") do - RubyUI.Avatar do - RubyUI.AvatarImage(src: "https://avatars.githubusercontent.com/u/246692?v=4", alt: "joeldrapper") - RubyUI.AvatarFallback { "JD" } - end - end - end - end - end - - assert_match(/joeldrapper/, output) +class RubyUI::HoverCardTest < Minitest::Test + def test_not_phlex + refute RubyUI::HoverCard.new.is_a?(Phlex::HTML) + end + + def test_data_controller + comp = RubyUI::HoverCard.new + assert_equal "ruby-ui--hover-card", comp.attrs.dig(:data, :controller) + end + + def test_default_options_delay + comp = RubyUI::HoverCard.new + options = JSON.parse(comp.attrs.dig(:data, :ruby_ui__hover_card_options_value)) + assert_equal [500, 250], options["delay"] + end + + def test_extra_attrs_pass_through + comp = RubyUI::HoverCard.new(id: "hc") + assert_equal "hc", comp.attrs[:id] + end +end + +class RubyUI::HoverCardContentTest < Minitest::Test + def test_not_phlex + refute RubyUI::HoverCardContent.new.is_a?(Phlex::HTML) + end + + def test_default_class + comp = RubyUI::HoverCardContent.new + assert_includes comp.attrs[:class], "z-50" + assert_includes comp.attrs[:class], "rounded-md" + end +end + +class RubyUI::HoverCardTriggerTest < Minitest::Test + def test_not_phlex + refute RubyUI::HoverCardTrigger.new.is_a?(Phlex::HTML) + end + + def test_data_target + comp = RubyUI::HoverCardTrigger.new + assert_equal "trigger", comp.attrs.dig(:data, :ruby_ui__hover_card_target) + end + + def test_default_class + comp = RubyUI::HoverCardTrigger.new + assert_includes comp.attrs[:class], "inline-block" end end diff --git a/test/ruby_ui/inline_code_test.rb b/test/ruby_ui/inline_code_test.rb index eb17bcd5..05036e3b 100644 --- a/test/ruby_ui/inline_code_test.rb +++ b/test/ruby_ui/inline_code_test.rb @@ -2,17 +2,23 @@ require "test_helper" -class RubyUI::InlineCodeTest < ComponentTest - def test_render_inline_code - output = phlex do - RubyUI::InlineCode() { "This is an inline code block" } - end +class RubyUI::InlineCodeTest < Minitest::Test + def test_not_phlex + refute RubyUI::InlineCode.new.is_a?(Phlex::HTML) + end + + def test_default_class + ic = RubyUI::InlineCode.new + assert_includes ic.attrs[:class], "bg-muted" + assert_includes ic.attrs[:class], "font-mono" + assert_includes ic.attrs[:class], "text-sm" + assert_includes ic.attrs[:class], "font-semibold" + assert_includes ic.attrs[:class], "rounded" + end - assert_match("This is an inline code block", output) - assert_match(/ruby-ui--sheet-content#close"}) { "Cancel" } - RubyUI.Button(type: "submit") { "Save" } - end - end - end - end - end - - assert_match(/Open Sheet/, output) +class RubyUI::SheetTest < Minitest::Test + def test_not_phlex + refute RubyUI::Sheet.new.is_a?(Phlex::HTML) + end + + def test_controller + assert_equal "ruby-ui--sheet", RubyUI::Sheet.new.attrs[:data][:controller] + end +end + +class RubyUI::SheetContentTest < Minitest::Test + def test_not_phlex + refute RubyUI::SheetContent.new.is_a?(Phlex::HTML) + end + + def test_default_side + assert_equal :right, RubyUI::SheetContent.new.side + end + + def test_side_classes_right + sc = RubyUI::SheetContent.new(side: :right) + assert_includes sc.attrs[:class], "inset-y-0" + end + + def test_side_classes_left + sc = RubyUI::SheetContent.new(side: :left) + assert_includes sc.attrs[:class], "inset-y-0" + assert_includes sc.attrs[:class], "left-0" + end + + def test_backdrop_attrs + sc = RubyUI::SheetContent.new + assert sc.backdrop_attrs[:class] + assert_includes sc.backdrop_attrs[:class], "backdrop-blur-sm" + end + + def test_has_default_class + assert_includes RubyUI::SheetContent.new.attrs[:class], "bg-background" + end +end + +class RubyUI::SheetDescriptionTest < Minitest::Test + def test_not_phlex + refute RubyUI::SheetDescription.new.is_a?(Phlex::HTML) + end + + def test_has_default_class + assert_includes RubyUI::SheetDescription.new.attrs[:class], "muted-foreground" + end +end + +class RubyUI::SheetFooterTest < Minitest::Test + def test_not_phlex + refute RubyUI::SheetFooter.new.is_a?(Phlex::HTML) + end + + def test_has_default_class + assert_includes RubyUI::SheetFooter.new.attrs[:class], "flex-col-reverse" + end +end + +class RubyUI::SheetHeaderTest < Minitest::Test + def test_not_phlex + refute RubyUI::SheetHeader.new.is_a?(Phlex::HTML) + end + + def test_has_default_class + assert_includes RubyUI::SheetHeader.new.attrs[:class], "space-y-1.5" + end +end + +class RubyUI::SheetMiddleTest < Minitest::Test + def test_not_phlex + refute RubyUI::SheetMiddle.new.is_a?(Phlex::HTML) + end + + def test_has_default_class + assert_includes RubyUI::SheetMiddle.new.attrs[:class], "py-4" + end +end + +class RubyUI::SheetTitleTest < Minitest::Test + def test_not_phlex + refute RubyUI::SheetTitle.new.is_a?(Phlex::HTML) + end + + def test_has_default_class + assert_includes RubyUI::SheetTitle.new.attrs[:class], "font-semibold" + end +end + +class RubyUI::SheetTriggerTest < Minitest::Test + def test_not_phlex + refute RubyUI::SheetTrigger.new.is_a?(Phlex::HTML) + end + + def test_data_action + assert_equal "click->ruby-ui--sheet#open", RubyUI::SheetTrigger.new.attrs[:data][:action] end end diff --git a/test/ruby_ui/shortcut_key_test.rb b/test/ruby_ui/shortcut_key_test.rb index 03c03a20..b902b9b2 100644 --- a/test/ruby_ui/shortcut_key_test.rb +++ b/test/ruby_ui/shortcut_key_test.rb @@ -2,15 +2,18 @@ require "test_helper" -class RubyUI::ShortcutKeyTest < ComponentTest - def test_render_with_all_items - output = phlex do - RubyUI.ShortcutKey do |shortcut| - shortcut.span(class: "text-xs") { "⌘" } - shortcut.plain "K" - end - end +class RubyUI::ShortcutKeyTest < Minitest::Test + def test_not_phlex + refute RubyUI::ShortcutKey.new.is_a?(Phlex::HTML) + end + + def test_has_default_class + assert_includes RubyUI::ShortcutKey.new.attrs[:class], "pointer-events-none" + end - assert_match(/K/, output) + def test_user_class_merged + sk = RubyUI::ShortcutKey.new(class: "custom") + assert_includes sk.attrs[:class], "custom" + assert_includes sk.attrs[:class], "pointer-events-none" end end diff --git a/test/ruby_ui/sidebar_test.rb b/test/ruby_ui/sidebar_test.rb index 93aa81e1..806785ff 100644 --- a/test/ruby_ui/sidebar_test.rb +++ b/test/ruby_ui/sidebar_test.rb @@ -2,164 +2,205 @@ require "test_helper" -class RubyUI::SidebarTest < ComponentTest - def test_render_with_all_items - output = phlex do - RubyUI.SidebarWrapper do - RubyUI.Sidebar do - RubyUI.SidebarHeader do - RubyUI.SidebarGroup do - RubyUI.SidebarGroupContent do - RubyUI.SidebarInput(id: "search", placeholder: "Search the docs") - end - end - end - RubyUI.SidebarContent do - RubyUI.SidebarGroup do - RubyUI.SidebarGroupLabel { "Application" } - RubyUI.SidebarGroupAction { "Group Action" } - RubyUI.SidebarGroupContent do - RubyUI.SidebarMenu do - RubyUI.SidebarMenuItem do - RubyUI.SidebarMenuSub do - RubyUI.SidebarMenuSubItem do - RubyUI.SidebarMenuSubButton(as: :a, href: "#") { "Sub Item 1" } - end - end - end - RubyUI.SidebarMenuItem do - RubyUI.SidebarMenuButton(as: :a, href: "#") { "Settings" } - RubyUI.SidebarMenuAction { "Settings" } - end - RubyUI.SidebarMenuItem do - RubyUI.SidebarMenuButton { "Dashboard" } - RubyUI.SidebarMenuAction { "Dashboard" } - RubyUI.SidebarMenuBadge { "Dashboard Badge" } - end - RubyUI.SidebarMenuItem do - RubyUI.SidebarMenuSkeleton() - end - end - end - end - RubyUI.SidebarSeparator() - end - RubyUI.SidebarFooter { "Footer" } - RubyUI.SidebarRail() - end - RubyUI.SidebarInset do - RubyUI.SidebarTrigger() - end - end - end - - assert_match(/Search the docs/, output) - assert_match(/Application/, output) - assert_match(/Group Action/, output) - assert_match(/Sub Item 1/, output) - assert_match(/Settings/, output) - assert_match(/Dashboard/, output) - assert_match(/Dashboard Badge/, output) - assert_match(/Footer/, output) - end - - def test_render_non_collapsible_sidebar - output = phlex do - RubyUI.SidebarWrapper do - RubyUI.Sidebar(collapsible: :none) do - RubyUI.SidebarHeader do - RubyUI.SidebarGroup do - RubyUI.SidebarGroupContent do - RubyUI.SidebarInput(id: "search", placeholder: "Search the docs") - end - end - end - RubyUI.SidebarContent do - RubyUI.SidebarGroup do - RubyUI.SidebarGroupLabel { "Application" } - RubyUI.SidebarGroupAction { "Group Action" } - RubyUI.SidebarGroupContent do - RubyUI.SidebarMenu do - RubyUI.SidebarMenuItem do - RubyUI.SidebarMenuSub do - RubyUI.SidebarMenuSubItem do - RubyUI.SidebarMenuSubButton(as: :a, href: "#") { "Sub Item 1" } - end - end - end - RubyUI.SidebarMenuItem do - RubyUI.SidebarMenuButton(as: :a, href: "#") { "Settings" } - RubyUI.SidebarMenuAction { "Settings" } - end - RubyUI.SidebarMenuItem do - RubyUI.SidebarMenuButton { "Dashboard" } - RubyUI.SidebarMenuAction { "Dashboard" } - RubyUI.SidebarMenuBadge { "Dashboard Badge" } - end - RubyUI.SidebarMenuItem do - RubyUI.SidebarMenuSkeleton() - end - end - end - end - RubyUI.SidebarSeparator() - end - RubyUI.SidebarFooter { "Footer" } - RubyUI.SidebarRail() - end - RubyUI.SidebarInset do - RubyUI.SidebarTrigger() - end - end - end - - assert_match(/Search the docs/, output) - assert_match(/Application/, output) - assert_match(/Group Action/, output) - assert_match(/Sub Item 1/, output) - assert_match(/Settings/, output) - assert_match(/Dashboard/, output) - assert_match(/Dashboard Badge/, output) - assert_match(/Footer/, output) - end - - def test_with_side_right - output = phlex do - RubyUI.Sidebar(side: :right) - end - - assert_match(/data-side="right"/, output) - end - - def test_with_variant_floating - output = phlex do - RubyUI.Sidebar(variant: :floating) - end - - assert_match(/data-variant="floating"/, output) - end - - def test_with_collapsible_icon - output = phlex do - RubyUI.Sidebar(collapsible: :icon) - end - - assert_match(/data-collapsible-kind="icon"/, output) - end - - def test_with_open_false - output = phlex do - RubyUI.Sidebar(open: false) - end - - assert_match(/data-state="collapsed"/, output) - end - - def test_with_collapsible_offcanvas - output = phlex do - RubyUI.Sidebar(collapsible: :offcanvas) - end - - assert_match(/data-collapsible-kind="offcanvas"/, output) +class RubyUI::SidebarTest < Minitest::Test + def test_not_phlex + refute RubyUI::Sidebar.new.is_a?(Phlex::HTML) + end + + def test_collapsible_by_default + sb = RubyUI::Sidebar.new + assert sb.collapsible? + end + + def test_none_collapsible + sb = RubyUI::Sidebar.new(collapsible: :none) + refute sb.collapsible? + end + + def test_default_side_left + sb = RubyUI::Sidebar.new + assert_equal :left, sb.side + end + + def test_side_right + sb = RubyUI::Sidebar.new(side: :right) + assert_equal :right, sb.side + end + + def test_invalid_side_raises + assert_raises(ArgumentError) { RubyUI::Sidebar.new(side: :top) } + end + + def test_invalid_collapsible_raises + assert_raises(ArgumentError) { RubyUI::Sidebar.new(collapsible: :invalid) } + end + + def test_open_by_default + sb = RubyUI::Sidebar.new + assert sb.open? + end + + def test_closed + sb = RubyUI::Sidebar.new(open: false) + refute sb.open? + end + + def test_collapsible_sidebar_data_expanded + cs = RubyUI::CollapsibleSidebar.new + assert_equal "expanded", cs.sidebar_data[:state] + assert_equal "sidebar", cs.sidebar_data[:ruby_ui__sidebar_target] + end + + def test_collapsible_sidebar_data_collapsed + cs = RubyUI::CollapsibleSidebar.new(open: false, collapsible: :offcanvas) + assert_equal "collapsed", cs.sidebar_data[:state] + assert_equal :offcanvas, cs.sidebar_data[:collapsible] + end + + def test_collapsible_sidebar_side_right + cs = RubyUI::CollapsibleSidebar.new(side: :right) + assert_includes cs.content_wrapper_class, "right-0" + refute_includes cs.content_wrapper_class, "left-0" + end + + def test_collapsible_sidebar_side_left + cs = RubyUI::CollapsibleSidebar.new(side: :left) + assert_includes cs.content_wrapper_class, "left-0" + end + + def test_non_collapsible_sidebar_class + nc = RubyUI::NonCollapsibleSidebar.new + assert_includes nc.attrs[:class], "bg-sidebar" + assert_includes nc.attrs[:class], "text-sidebar-foreground" + end + + def test_mobile_sidebar_target + ms = RubyUI::MobileSidebar.new + assert_equal "mobileSidebar", ms.attrs.dig(:data, :ruby_ui__sidebar_target) + end + + def test_sidebar_wrapper_has_controller + sw = RubyUI::SidebarWrapper.new + assert_equal "ruby-ui--sidebar", sw.attrs.dig(:data, :controller) + end + + def test_sidebar_wrapper_has_css_vars + sw = RubyUI::SidebarWrapper.new + assert_includes sw.attrs[:style], "--sidebar-width" + end + + def test_sidebar_content_data + sc = RubyUI::SidebarContent.new + assert_equal "content", sc.attrs.dig(:data, :sidebar) + end + + def test_sidebar_header_data + sh = RubyUI::SidebarHeader.new + assert_equal "header", sh.attrs.dig(:data, :sidebar) + end + + def test_sidebar_footer_data + sf = RubyUI::SidebarFooter.new + assert_equal "footer", sf.attrs.dig(:data, :sidebar) + end + + def test_sidebar_group_data + sg = RubyUI::SidebarGroup.new + assert_equal "group", sg.attrs.dig(:data, :sidebar) + end + + def test_sidebar_group_label_data + sgl = RubyUI::SidebarGroupLabel.new + assert_equal "group-label", sgl.attrs.dig(:data, :sidebar) + end + + def test_sidebar_group_action_default_tag + sga = RubyUI::SidebarGroupAction.new + assert_equal :button, sga.tag_name + end + + def test_sidebar_group_content_data + sgc = RubyUI::SidebarGroupContent.new + assert_equal "group-content", sgc.attrs.dig(:data, :sidebar) + end + + def test_sidebar_inset_class + si = RubyUI::SidebarInset.new + assert_includes si.attrs[:class], "bg-background" + end + + def test_sidebar_input_data + si = RubyUI::SidebarInput.new + assert_equal "input", si.attrs.dig(:data, :sidebar) + end + + def test_sidebar_separator_data + ss = RubyUI::SidebarSeparator.new + assert_equal "separator", ss.attrs.dig(:data, :sidebar) + end + + def test_sidebar_menu_data + sm = RubyUI::SidebarMenu.new + assert_equal "menu", sm.attrs.dig(:data, :sidebar) + end + + def test_sidebar_menu_item_data + smi = RubyUI::SidebarMenuItem.new + assert_equal "menu-item", smi.attrs.dig(:data, :sidebar) + end + + def test_sidebar_menu_button_data + smb = RubyUI::SidebarMenuButton.new + assert_equal "menu-button", smb.attrs.dig(:data, :sidebar) + end + + def test_sidebar_menu_button_invalid_variant_raises + assert_raises(ArgumentError) { RubyUI::SidebarMenuButton.new(variant: :invalid) } + end + + def test_sidebar_menu_action_data + sma = RubyUI::SidebarMenuAction.new + assert_equal "menu-action", sma.attrs.dig(:data, :sidebar) + end + + def test_sidebar_menu_badge_data + smb = RubyUI::SidebarMenuBadge.new + assert_equal "menu-badge", smb.attrs.dig(:data, :sidebar) + end + + def test_sidebar_menu_skeleton_data + sms = RubyUI::SidebarMenuSkeleton.new + assert_equal "menu-skeleton", sms.attrs.dig(:data, :sidebar) + refute sms.show_icon? + end + + def test_sidebar_menu_skeleton_show_icon + sms = RubyUI::SidebarMenuSkeleton.new(show_icon: true) + assert sms.show_icon? + end + + def test_sidebar_menu_sub_data + sms = RubyUI::SidebarMenuSub.new + assert_equal "menu-sub", sms.attrs.dig(:data, :sidebar) + end + + def test_sidebar_menu_sub_button_size_classes + smsb = RubyUI::SidebarMenuSubButton.new(size: :sm) + assert_includes smsb.attrs[:class], "text-xs" + end + + def test_sidebar_menu_sub_button_invalid_size_raises + assert_raises(ArgumentError) { RubyUI::SidebarMenuSubButton.new(size: :xl) } + end + + def test_sidebar_rail_data + sr = RubyUI::SidebarRail.new + assert_equal "rail", sr.attrs.dig(:data, :sidebar) + end + + def test_sidebar_trigger_data + st = RubyUI::SidebarTrigger.new + assert_equal "trigger", st.attrs.dig(:data, :sidebar) + assert_includes st.attrs[:class], "h-7" end end diff --git a/test/ruby_ui/skeleton_test.rb b/test/ruby_ui/skeleton_test.rb index 2b136011..cd1555eb 100644 --- a/test/ruby_ui/skeleton_test.rb +++ b/test/ruby_ui/skeleton_test.rb @@ -2,14 +2,19 @@ require "test_helper" -class RubyUI::SkeletonTest < ComponentTest - def test_render - output = phlex do - RubyUI::Skeleton(class: "w-14 h-14") - end - - assert_match(/div/, output) - assert_match(/w-14/, output) - assert_match(/h-14/, output) +class RubyUI::SkeletonTest < Minitest::Test + def test_not_phlex + refute RubyUI::Skeleton.new.is_a?(Phlex::HTML) + end + + def test_has_default_class + assert_includes RubyUI::Skeleton.new.attrs[:class], "animate-pulse" + end + + def test_user_class_merged + s = RubyUI::Skeleton.new(class: "w-14 h-14") + assert_includes s.attrs[:class], "w-14" + assert_includes s.attrs[:class], "h-14" + assert_includes s.attrs[:class], "animate-pulse" end end diff --git a/test/ruby_ui/switch_test.rb b/test/ruby_ui/switch_test.rb index 59fa4cfc..f147abab 100644 --- a/test/ruby_ui/switch_test.rb +++ b/test/ruby_ui/switch_test.rb @@ -2,20 +2,54 @@ require "test_helper" -class RubyUI::SwitchTest < ComponentTest - def test_render_with_all_items - output = phlex do - RubyUI.Switch(name: "toggle") - end +class RubyUI::SwitchTest < Minitest::Test + def test_not_phlex + refute RubyUI::Switch.new.is_a?(Phlex::HTML) + end + + def test_include_hidden_true_by_default + sw = RubyUI::Switch.new(name: "toggle") + assert sw.include_hidden? + assert_equal "toggle", sw.hidden_input_attrs[:name] + assert_equal "0", sw.hidden_input_attrs[:value] + end + + def test_include_hidden_false + sw = RubyUI::Switch.new(name: "toggle", include_hidden: false) + refute sw.include_hidden? + end + + def test_checkbox_attrs_type_and_value + sw = RubyUI::Switch.new(name: "toggle") + assert_equal "checkbox", sw.checkbox_attrs[:type] + assert_equal "1", sw.checkbox_attrs[:value] + assert_equal "toggle", sw.checkbox_attrs[:name] + end + + def test_custom_checked_value + sw = RubyUI::Switch.new(checked_value: "true", unchecked_value: "false") + assert_equal "true", sw.checkbox_attrs[:value] + assert_equal "false", sw.hidden_input_attrs[:value] + end - assert_match(/toggle/, output) + def test_label_classes_include_rounded_full + sw = RubyUI::Switch.new + assert_includes sw.label_classes, "rounded-full" + assert_includes sw.label_classes, "bg-input" end - def test_render_checked - output = phlex do - RubyUI.Switch(name: "toggle", checked: true) - end + def test_thumb_classes_include_translate + sw = RubyUI::Switch.new + assert_includes sw.thumb_classes, "peer-checked:translate-x-5" + end + + def test_checked_attr_passes_through + sw = RubyUI::Switch.new(name: "toggle", checked: true) + assert sw.checkbox_attrs[:checked] + end - assert_match(/checked/, output) + def test_extra_attrs_pass_through + sw = RubyUI::Switch.new(name: "foo", data: {controller: "bar"}) + assert_equal({controller: "bar"}, sw.checkbox_attrs[:data]) end end diff --git a/test/ruby_ui/table_test.rb b/test/ruby_ui/table_test.rb index 2ae1e4ba..0d107351 100644 --- a/test/ruby_ui/table_test.rb +++ b/test/ruby_ui/table_test.rb @@ -2,45 +2,58 @@ require "test_helper" -class RubyUI::TableTest < ComponentTest - def test_render_with_all_items - invoices = [ - {identifier: "INV-0001", status: "Active", method: "Credit Card", amount: 100}, - {identifier: "INV-0002", status: "Active", method: "Bank Transfer", amount: 230}, - {identifier: "INV-0003", status: "Pending", method: "PayPal", amount: 350}, - {identifier: "INV-0004", status: "Inactive", method: "Credit Card", amount: 100} - ] - - output = phlex do - RubyUI.Table do - RubyUI.TableCaption { "Employees at Acme inc." } - RubyUI.TableHeader do - RubyUI.TableRow do - RubyUI.TableHead { "Name" } - RubyUI.TableHead { "Email" } - RubyUI.TableHead { "Status" } - RubyUI.TableHead(class: "text-right") { "Role" } - end - end - RubyUI.TableBody do - invoices.each do |invoice| - RubyUI.TableRow do - RubyUI.TableCell(class: "font-medium") { invoice[:identifier] } - RubyUI.TableCell { invoice[:status] } - RubyUI.TableCell { invoice[:method] } - RubyUI.TableCell(class: "text-right") { invoice[:amount] } - end - end - end - RubyUI.TableFooter do - RubyUI.TableRow do - RubyUI.TableHead(class: "font-medium", colspan: 3) { "Total" } - RubyUI.TableHead(class: "font-medium text-right") { invoices.sum { |invoice| invoice[:amount] } } - end - end - end - end - - assert_match(/Total/, output) +class RubyUI::TableTest < Minitest::Test + def test_not_phlex + refute RubyUI::Table.new.is_a?(Phlex::HTML) + end + + def test_has_default_class + assert RubyUI::Table.new.attrs[:class] + assert_includes RubyUI::Table.new.attrs[:class], "w-full" + assert_includes RubyUI::Table.new.attrs[:class], "caption-bottom" + end + + def test_user_class_merges + t = RubyUI::Table.new(class: "extra-class") + assert_includes t.attrs[:class], "extra-class" + assert_includes t.attrs[:class], "w-full" + end + + def test_table_body_default_class + assert_includes RubyUI::TableBody.new.attrs[:class], "[&_tr:last-child]:border-0" + end + + def test_table_caption_default_class + assert_includes RubyUI::TableCaption.new.attrs[:class], "mt-4" + assert_includes RubyUI::TableCaption.new.attrs[:class], "text-muted-foreground" + end + + def test_table_cell_default_class + assert_includes RubyUI::TableCell.new.attrs[:class], "p-2" + assert_includes RubyUI::TableCell.new.attrs[:class], "align-middle" + end + + def test_table_footer_default_class + assert_includes RubyUI::TableFooter.new.attrs[:class], "bg-muted/50" + end + + def test_table_head_default_class + assert_includes RubyUI::TableHead.new.attrs[:class], "h-10" + assert_includes RubyUI::TableHead.new.attrs[:class], "text-muted-foreground" + end + + def test_table_header_default_class + assert_includes RubyUI::TableHeader.new.attrs[:class], "[&_tr]:border-b" + end + + def test_table_row_default_class + assert_includes RubyUI::TableRow.new.attrs[:class], "border-b" + assert_includes RubyUI::TableRow.new.attrs[:class], "hover:bg-muted/50" + end + + def test_extra_attrs_pass_through + t = RubyUI::Table.new(id: "my-table", data: {controller: "foo"}) + assert_equal "my-table", t.attrs[:id] + assert_equal({controller: "foo"}, t.attrs[:data]) end end diff --git a/test/ruby_ui/tabs_test.rb b/test/ruby_ui/tabs_test.rb index f0e028e6..b9ba6216 100644 --- a/test/ruby_ui/tabs_test.rb +++ b/test/ruby_ui/tabs_test.rb @@ -2,26 +2,61 @@ require "test_helper" -class RubyUI::TabsTest < ComponentTest - def test_render_with_all_items - output = phlex do - RubyUI.Tabs(default_value: "account", class: "w-96") do - RubyUI.TabsList do - RubyUI.TabsTrigger(value: "account") { "Account" } - RubyUI.TabsTrigger(value: "password") { "Password" } - end - RubyUI.TabsContent(value: "account") do - RubyUI::Text(as: "p", size: "4") { "Account" } - RubyUI::Text(size: "5", weight: "semibold") { "Are you sure absolutely sure?" } - RubyUI::Text(size: "2", class: "text-muted-foreground") { "Update your account details." } - end - RubyUI.TabsContent(value: "password") do - RubyUI::Text(as: "p", size: "4") { "Password" } - RubyUI::Text(size: "2", class: "text-muted-foreground") { "Change your password here. After saving, you'll be logged out." } - end - end - end - - assert_match(/Account/, output) +class RubyUI::TabsTest < Minitest::Test + def test_not_phlex + refute RubyUI::Tabs.new.is_a?(Phlex::HTML) + end + + def test_controller + assert_equal "ruby-ui--tabs", RubyUI::Tabs.new.attrs[:data][:controller] + end + + def test_default_active_value + t = RubyUI::Tabs.new(default: "account") + assert_equal "account", t.attrs[:data][:ruby_ui__tabs_active_value] + end +end + +class RubyUI::TabsContentTest < Minitest::Test + def test_not_phlex + refute RubyUI::TabsContent.new(value: "tab1").is_a?(Phlex::HTML) + end + + def test_value_stored + tc = RubyUI::TabsContent.new(value: "tab1") + assert_equal "tab1", tc.value + end + + def test_has_default_class + assert_includes RubyUI::TabsContent.new(value: "x").attrs[:class], "hidden" + end +end + +class RubyUI::TabsListTest < Minitest::Test + def test_not_phlex + refute RubyUI::TabsList.new.is_a?(Phlex::HTML) + end + + def test_has_default_class + assert_includes RubyUI::TabsList.new.attrs[:class], "bg-muted" + end +end + +class RubyUI::TabsTriggerTest < Minitest::Test + def test_not_phlex + refute RubyUI::TabsTrigger.new(value: "tab1").is_a?(Phlex::HTML) + end + + def test_value_stored + tt = RubyUI::TabsTrigger.new(value: "tab1") + assert_equal "tab1", tt.value + end + + def test_type + assert_equal :button, RubyUI::TabsTrigger.new(value: "x").attrs[:type] + end + + def test_has_default_class + assert_includes RubyUI::TabsTrigger.new(value: "x").attrs[:class], "rounded-md" end end diff --git a/test/ruby_ui/text_test.rb b/test/ruby_ui/text_test.rb index 1817688e..eda446d3 100644 --- a/test/ruby_ui/text_test.rb +++ b/test/ruby_ui/text_test.rb @@ -2,106 +2,117 @@ require "test_helper" -class RubyUI::TypographyTest < ComponentTest - def test_heading_with_levels - (1..4).each do |level| - output = phlex do - RubyUI::Heading(level: level.to_s) { "This is an H#{level} title" } - end +class RubyUI::TextTest < Minitest::Test + def test_not_phlex + refute RubyUI::Text.new.is_a?(Phlex::HTML) + end - assert_match("This is an H#{level} title", output) - assert_match(/