Skip to content

Commit c0eece6

Browse files
add search input component
1 parent 96fdf7b commit c0eece6

File tree

8 files changed

+234
-5
lines changed

8 files changed

+234
-5
lines changed

app/assets/stylesheets/css/components/input.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@
66
@import "./input/states.css";
77
@import "./input/password.css";
88
@import "./input/date.css";
9+
@import "./input/search_input.css";
10+
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/* Search Input — extends base input styles, adds left icon + optional right shortcut */
2+
/* BEM: .search-input (block), .search-input__* (element), .search-input--* (modifier) */
3+
/* States (focus, hover, disabled, etc.) inherit from input base & states.css */
4+
5+
.search-input {
6+
@apply relative w-full;
7+
}
8+
9+
.search-input__prefix {
10+
@apply absolute inset-y-0 start-0 flex items-center pointer-events-none ps-2 text-content-secondary z-10;
11+
}
12+
13+
/* Override input padding for icons — base uses px-2 */
14+
.search-input__input {
15+
@apply ps-8 pe-2;
16+
}
17+
18+
.search-input__input--with-shortcut {
19+
@apply pe-16;
20+
}
21+
22+
.search-input__suffix {
23+
@apply absolute inset-y-0 end-0 flex items-center pe-2 text-content-secondary pointer-events-none z-10 min-w-[18px] gap-1;
24+
}
25+
26+
.search-input__shortcut {
27+
@apply text-[10px] leading-[14px] font-semibold py-px px-1 border border-secondary rounded-sm bg-secondary min-w-5 flex items-center justify-center;
28+
29+
box-shadow: 0 -1.5px 0 1px var(--color-secondary) inset, 0 0.8px 0 1.3px var(--color-primary) inset;
30+
}
31+
32+
/* ==========================================================================
33+
Size variants — icon scales with input size (like password.css)
34+
Input must come first in DOM for sibling selector to work
35+
========================================================================== */
36+
37+
.search-input__prefix svg {
38+
@apply shrink-0;
39+
}
40+
41+
.search-input__input.input--size-sm ~ .search-input__prefix svg {
42+
@apply size-3.5;
43+
}
44+
45+
.search-input__prefix svg,
46+
.search-input__input.input--size-lg ~ .search-input__prefix svg,
47+
.search-input__input.input--size-md ~ .search-input__prefix svg {
48+
@apply size-4;
49+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<%= content_tag :div, class: "search-input" do %>
2+
<%= search_field_tag @name, @value,
3+
id: @id,
4+
placeholder: @placeholder,
5+
class: class_names(
6+
"search-input__input",
7+
{"search-input__input--with-shortcut": @with_shortcut},
8+
@classes
9+
),
10+
disabled: @disabled,
11+
autocomplete: "off",
12+
data: @data
13+
%>
14+
15+
<span class="search-input__prefix" aria-hidden="true">
16+
<%= helpers.svg "tabler/outline/search" %>
17+
</span>
18+
19+
<% if @with_shortcut %>
20+
<span class="search-input__suffix" aria-hidden="true">
21+
<kbd class="search-input__shortcut">
22+
<abbr title="Command" class="no-underline pc:hidden"></abbr>
23+
<abbr title="CTRL" class="no-underline mac:hidden">CTRL + </abbr>
24+
</kbd>
25+
<kbd class="search-input__shortcut">K</kbd>
26+
</span>
27+
<% end %>
28+
<% end %>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# frozen_string_literal: true
2+
3+
class Avo::UI::SearchInputComponent < Avo::BaseComponent
4+
prop :name, default: "q"
5+
prop :id
6+
prop :value
7+
prop :placeholder
8+
prop :disabled, default: false
9+
prop :with_shortcut, default: false
10+
prop :classes
11+
prop :data, default: -> { {} }
12+
end

app/components/avo/views/resource_index_component.html.erb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
<%= render ui.panel(
1313
cover: resource.cover,
14-
class: "w-full",
14+
class: "w-full px-2",
1515
data: {
1616
component_name:,
1717
controller: "resource-search",
@@ -59,15 +59,15 @@
5959
<div class="flex flex-col space-y-2 py-4 xs:flex-row xs:justify-between xs:space-y-0 <%= "hidden" unless header_visible? %>">
6060
<div class="flex w-64 items-center">
6161
<% if show_search_input %>
62-
<%= text_field_tag "q", params[:q],
63-
id: nil,
62+
<%= render ui.search_input(
63+
name: "q",
64+
value: params[:q],
6465
placeholder: t('avo.search.placeholder'),
65-
class: "block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm",
6666
data: {
6767
action: "input->resource-search#search",
6868
"resource-search-target": "input"
6969
},
70-
autocomplete: "off" %>
70+
) %>
7171
<% else %>
7272
<%# Offset for the space-y-2 property when the search is missing %>
7373
<div class="-mb-2"></div>
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# frozen_string_literal: true
2+
3+
require "rails_helper"
4+
5+
RSpec.describe Avo::UI::SearchInputComponent, type: :component do
6+
describe "rendering" do
7+
it "renders wrapper with search-input class" do
8+
render_inline(described_class.new(name: "q", placeholder: "Search"))
9+
10+
expect(page).to have_css("div.search-input")
11+
expect(page).to have_css("input[type='search'][name='q'][placeholder='Search']")
12+
end
13+
14+
it "passes data attributes to the input for resource-search controller" do
15+
render_inline(described_class.new(
16+
name: "q",
17+
data: {
18+
action: "input->resource-search#search",
19+
"resource-search-target": "input"
20+
}
21+
))
22+
23+
input = page.find("input[name='q']")
24+
expect(input["data-resource-search-target"]).to eq("input")
25+
expect(input["data-action"]).to eq("input->resource-search#search")
26+
end
27+
28+
it "renders search icon prefix" do
29+
render_inline(described_class.new(name: "q"))
30+
31+
expect(page).to have_css(".search-input__prefix")
32+
expect(page).to have_css(".search-input__prefix svg")
33+
end
34+
35+
it "renders shortcut suffix when with_shortcut is true" do
36+
render_inline(described_class.new(name: "q", with_shortcut: true))
37+
38+
expect(page).to have_css(".search-input__suffix")
39+
expect(page).to have_css(".search-input__shortcut", count: 2)
40+
end
41+
42+
it "does not render shortcut suffix when with_shortcut is false" do
43+
render_inline(described_class.new(name: "q", with_shortcut: false))
44+
45+
expect(page).not_to have_css(".search-input__suffix")
46+
end
47+
48+
it "renders with value when provided" do
49+
render_inline(described_class.new(name: "q", value: "hello"))
50+
51+
expect(page).to have_css("input[name='q'][value='hello']")
52+
end
53+
end
54+
end
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# frozen_string_literal: true
2+
3+
module UI
4+
class SearchInputComponentPreview < ViewComponent::Preview
5+
layout "component_preview"
6+
7+
# @!group Examples
8+
9+
# Search input: 3 columns (sm, md, lg) and 2 rows (with shortcut, without shortcut)
10+
def default
11+
render_with_template(template: "u_i/search_input_component_preview/default")
12+
end
13+
14+
# @!endgroup
15+
end
16+
end
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<div class="p-8 space-y-8">
2+
<div class="text-lg font-semibold text-content mb-6">Search Input — Size variants & shortcut</div>
3+
4+
<div class="grid grid-cols-3 gap-6">
5+
<%# Header row: sizes %>
6+
<div class="text-sm font-semibold text-content">Small (sm)</div>
7+
<div class="text-sm font-semibold text-content">Medium (md)</div>
8+
<div class="text-sm font-semibold text-content">Large (lg)</div>
9+
10+
<%# Row 1: With shortcut (⌘K) %>
11+
<div>
12+
<div class="text-xs font-medium text-content-secondary mb-2">With shortcut</div>
13+
<%= render Avo::UI::SearchInputComponent.new(
14+
name: "q_sm",
15+
placeholder: "Search",
16+
with_shortcut: true,
17+
classes: "w-full input--size-sm"
18+
) %>
19+
</div>
20+
<div>
21+
<div class="text-xs font-medium text-content-secondary mb-2">With shortcut</div>
22+
<%= render Avo::UI::SearchInputComponent.new(
23+
name: "q_md",
24+
placeholder: "Search",
25+
with_shortcut: true,
26+
classes: "w-full input--size-md"
27+
) %>
28+
</div>
29+
<div>
30+
<div class="text-xs font-medium text-content-secondary mb-2">With shortcut</div>
31+
<%= render Avo::UI::SearchInputComponent.new(
32+
name: "q_lg",
33+
placeholder: "Search",
34+
with_shortcut: true,
35+
classes: "w-full input--size-lg"
36+
) %>
37+
</div>
38+
39+
<%# Row 2: Without shortcut %>
40+
<div>
41+
<div class="text-xs font-medium text-content-secondary mb-2">Without shortcut</div>
42+
<%= render Avo::UI::SearchInputComponent.new(
43+
name: "q_sm_no",
44+
placeholder: "Search",
45+
with_shortcut: false,
46+
classes: "w-full input--size-sm"
47+
) %>
48+
</div>
49+
<div>
50+
<div class="text-xs font-medium text-content-secondary mb-2">Without shortcut</div>
51+
<%= render Avo::UI::SearchInputComponent.new(
52+
name: "q_md_no",
53+
placeholder: "Search",
54+
with_shortcut: false,
55+
classes: "w-full input--size-md"
56+
) %>
57+
</div>
58+
<div>
59+
<div class="text-xs font-medium text-content-secondary mb-2">Without shortcut</div>
60+
<%= render Avo::UI::SearchInputComponent.new(
61+
name: "q_lg_no",
62+
placeholder: "Search",
63+
with_shortcut: false,
64+
classes: "w-full input--size-lg"
65+
) %>
66+
</div>
67+
</div>
68+
</div>

0 commit comments

Comments
 (0)