lightning_ui_kit 0.3.3 → 0.3.4
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/README.md +2 -2
- data/app/assets/stylesheets/lightning_ui_kit/application.css +1 -0
- data/app/assets/stylesheets/lightning_ui_kit/themes.css +15 -2
- data/app/assets/vendor/lightning_ui_kit.css +404 -68
- data/app/assets/vendor/lightning_ui_kit.js +3 -3
- data/app/components/lightning_ui_kit/accordion/item_component.html.erb +30 -0
- data/app/components/lightning_ui_kit/accordion/item_component.rb +13 -0
- data/app/components/lightning_ui_kit/accordion_component.html.erb +5 -0
- data/app/components/lightning_ui_kit/accordion_component.rb +22 -0
- data/app/components/lightning_ui_kit/alert_component.html.erb +14 -3
- data/app/components/lightning_ui_kit/alert_component.rb +58 -5
- data/app/components/lightning_ui_kit/card_component.html.erb +34 -0
- data/app/components/lightning_ui_kit/card_component.rb +22 -0
- data/app/components/lightning_ui_kit/layout_component.html.erb +3 -3
- data/app/components/lightning_ui_kit/radio_group/option_component.html.erb +1 -0
- data/app/components/lightning_ui_kit/radio_group/option_component.rb +14 -0
- data/app/components/lightning_ui_kit/radio_group_component.html.erb +60 -0
- data/app/components/lightning_ui_kit/radio_group_component.rb +70 -0
- data/app/components/lightning_ui_kit/tabs/tab_component.html.erb +1 -0
- data/app/components/lightning_ui_kit/tabs/tab_component.rb +8 -0
- data/app/components/lightning_ui_kit/tabs_component.html.erb +30 -0
- data/app/components/lightning_ui_kit/tabs_component.rb +65 -0
- data/app/javascript/lightning_ui_kit/controllers/radio_group_controller.js +74 -0
- data/app/javascript/lightning_ui_kit/controllers/tabs_controller.js +77 -0
- data/app/javascript/lightning_ui_kit/index.js +6 -2
- data/lib/lightning_ui_kit/builder.rb +16 -4
- data/lib/lightning_ui_kit/version.rb +1 -1
- metadata +18 -4
- data/app/components/lightning_ui_kit/banner_component.html.erb +0 -17
- data/app/components/lightning_ui_kit/banner_component.rb +0 -33
- /data/app/javascript/lightning_ui_kit/controllers/{banner_controller.js → alert_controller.js} +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class LightningUiKit::AccordionComponent < LightningUiKit::BaseComponent
|
|
2
|
+
renders_many :items, LightningUiKit::Accordion::ItemComponent
|
|
3
|
+
|
|
4
|
+
def initialize(open_first: true, **options)
|
|
5
|
+
@open_first = open_first
|
|
6
|
+
@options = options
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def classes
|
|
10
|
+
merge_classes([
|
|
11
|
+
"lui:divide-y lui:divide-border",
|
|
12
|
+
@options[:class]
|
|
13
|
+
].compact.join(" "))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def data
|
|
17
|
+
{
|
|
18
|
+
controller: "lui-accordion",
|
|
19
|
+
lui_accordion_open_first_value: @open_first
|
|
20
|
+
}.merge(@options[:data] || {})
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
<%= tag.div
|
|
2
|
-
<%= heroicon(
|
|
3
|
-
|
|
1
|
+
<%= tag.div(class: classes, role: "alert", data: @title ? { type: @type, controller: "lui-alert" } : { type: @type }) do %>
|
|
2
|
+
<%= heroicon(icon, variant: @title ? :outline : :solid, options: { class: icon_classes }) %>
|
|
3
|
+
<span class="lui:sr-only"><%= @type.to_s.humanize %></span>
|
|
4
|
+
<% if @title %>
|
|
5
|
+
<h5 class="<%= title_classes %>"><%= @title %></h5>
|
|
6
|
+
<div class="lui:text-foreground-secondary"><%= content %></div>
|
|
7
|
+
<% if footer.present? %>
|
|
8
|
+
<div class="lui:mt-3">
|
|
9
|
+
<%= footer %>
|
|
10
|
+
</div>
|
|
11
|
+
<% end %>
|
|
12
|
+
<% else %>
|
|
13
|
+
<%= content %>
|
|
14
|
+
<% end %>
|
|
4
15
|
<% end %>
|
|
@@ -1,14 +1,67 @@
|
|
|
1
1
|
class LightningUiKit::AlertComponent < LightningUiKit::BaseComponent
|
|
2
|
-
|
|
2
|
+
renders_one :footer
|
|
3
|
+
|
|
4
|
+
def initialize(title: nil, type: :info, **options)
|
|
5
|
+
@title = title
|
|
3
6
|
@type = type
|
|
4
7
|
@options = options
|
|
5
8
|
end
|
|
6
9
|
|
|
7
|
-
def
|
|
8
|
-
|
|
10
|
+
def classes
|
|
11
|
+
merge_classes([base_classes, type_classes, @options[:class]].compact.join(" "))
|
|
9
12
|
end
|
|
10
13
|
|
|
11
|
-
def
|
|
12
|
-
|
|
14
|
+
def icon
|
|
15
|
+
case @type
|
|
16
|
+
when :error then "exclamation-triangle"
|
|
17
|
+
when :success then "check-circle"
|
|
18
|
+
when :warning then "exclamation-triangle"
|
|
19
|
+
else "information-circle"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def icon_classes
|
|
24
|
+
base = if @title
|
|
25
|
+
"lui:absolute lui:left-4 lui:top-4 lui:size-4"
|
|
26
|
+
else
|
|
27
|
+
"lui:size-4 lui:me-3 lui:shrink-0"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
case @type
|
|
31
|
+
when :error then "#{base} lui:text-destructive-text"
|
|
32
|
+
when :success then "#{base} lui:text-success-text"
|
|
33
|
+
when :warning then "#{base} lui:text-warning-text"
|
|
34
|
+
else "#{base} lui:text-foreground"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def title_classes
|
|
39
|
+
base = "lui:mb-1 lui:font-medium lui:leading-none lui:tracking-tight"
|
|
40
|
+
case @type
|
|
41
|
+
when :error then "#{base} lui:text-destructive-text"
|
|
42
|
+
when :success then "#{base} lui:text-success-text"
|
|
43
|
+
when :warning then "#{base} lui:text-warning-text"
|
|
44
|
+
else base
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def base_classes
|
|
51
|
+
shared = "lui:text-sm lui:text-foreground lui:rounded-lg lui:border lui:transition-opacity lui:duration-300 lui:ease-out lui:opacity-100"
|
|
52
|
+
if @title
|
|
53
|
+
"lui:relative lui:w-full #{shared} lui:py-4 lui:pl-11 lui:pr-4"
|
|
54
|
+
else
|
|
55
|
+
"lui:flex lui:items-center #{shared} lui:p-4"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def type_classes
|
|
60
|
+
case @type
|
|
61
|
+
when :error then "lui:border-destructive-border/50"
|
|
62
|
+
when :success then "lui:border-success-indicator/50"
|
|
63
|
+
when :warning then "lui:border-warning-indicator/50"
|
|
64
|
+
else "lui:border-border"
|
|
65
|
+
end
|
|
13
66
|
end
|
|
14
67
|
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<%= tag.div(class: classes, data: data) do %>
|
|
2
|
+
<% if header? || title? || description? || action? %>
|
|
3
|
+
<div data-slot="card-header" class="lui:grid lui:auto-rows-min lui:grid-rows-[auto_auto] lui:items-start lui:gap-1.5 lui:px-6 <%= 'lui:grid-cols-[1fr_auto]' if action? %>">
|
|
4
|
+
<% if header? %>
|
|
5
|
+
<%= header %>
|
|
6
|
+
<% end %>
|
|
7
|
+
<% if title? %>
|
|
8
|
+
<div data-slot="card-title" class="lui:text-base lui:leading-none lui:font-semibold lui:tracking-tight lui:text-foreground">
|
|
9
|
+
<%= title %>
|
|
10
|
+
</div>
|
|
11
|
+
<% end %>
|
|
12
|
+
<% if description? %>
|
|
13
|
+
<div data-slot="card-description" class="lui:text-foreground-muted lui:text-sm/relaxed">
|
|
14
|
+
<%= description %>
|
|
15
|
+
</div>
|
|
16
|
+
<% end %>
|
|
17
|
+
<% if action? %>
|
|
18
|
+
<div data-slot="card-action" class="lui:col-start-2 lui:row-span-2 lui:row-start-1 lui:self-start lui:justify-self-end">
|
|
19
|
+
<%= action %>
|
|
20
|
+
</div>
|
|
21
|
+
<% end %>
|
|
22
|
+
</div>
|
|
23
|
+
<% end %>
|
|
24
|
+
<% if content.present? %>
|
|
25
|
+
<div data-slot="card-content" class="lui:px-6">
|
|
26
|
+
<%= content %>
|
|
27
|
+
</div>
|
|
28
|
+
<% end %>
|
|
29
|
+
<% if footer? %>
|
|
30
|
+
<div data-slot="card-footer" class="lui:flex lui:items-center lui:gap-4 lui:border-t lui:border-border lui:px-6 lui:pt-6">
|
|
31
|
+
<%= footer %>
|
|
32
|
+
</div>
|
|
33
|
+
<% end %>
|
|
34
|
+
<% end %>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class LightningUiKit::CardComponent < LightningUiKit::BaseComponent
|
|
2
|
+
renders_one :header
|
|
3
|
+
renders_one :title
|
|
4
|
+
renders_one :description
|
|
5
|
+
renders_one :footer
|
|
6
|
+
renders_one :action
|
|
7
|
+
|
|
8
|
+
def initialize(**options)
|
|
9
|
+
@options = options
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def classes
|
|
13
|
+
merge_classes([
|
|
14
|
+
"lui:flex lui:flex-col lui:gap-6 lui:rounded-xl lui:border lui:border-border lui:bg-surface lui:py-6 lui:text-foreground lui:shadow-sm",
|
|
15
|
+
@options[:class]
|
|
16
|
+
].compact.join(" "))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def data
|
|
20
|
+
@options[:data] || {}
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
<div class="lui:relative lui:isolate lui:flex lui:h-screen lui:w-full lui:bg-surface lui:max-lg:flex-col lui:max-lg:h-auto lui:max-lg:min-h-screen lui:
|
|
1
|
+
<div class="lui-page-gradient lui:relative lui:isolate lui:flex lui:h-screen lui:w-full lui:bg-surface-page lui:max-lg:flex-col lui:max-lg:h-auto lui:max-lg:min-h-screen lui:overflow-hidden" data-controller="lui-layout">
|
|
2
2
|
<%# Desktop Sidebar (fixed, hidden on mobile) %>
|
|
3
3
|
<div class="lui:fixed lui:inset-y-0 lui:left-0 <%= sidebar_width_class %> lui:max-lg:hidden lui:z-10">
|
|
4
|
-
<nav class="lui:flex lui:h-full lui:min-h-0 lui:flex-col">
|
|
4
|
+
<nav class="lui:flex lui:h-full lui:min-h-0 lui:flex-col lui:bg-surface-aside lui:backdrop-blur-[24px] lui:backdrop-saturate-150">
|
|
5
5
|
<%# Sidebar content %>
|
|
6
6
|
<div class="lui:flex lui:flex-1 lui:flex-col lui:overflow-y-auto lui:p-4">
|
|
7
7
|
<% if header? %>
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
></div>
|
|
65
65
|
|
|
66
66
|
<%# Mobile Sidebar Panel %>
|
|
67
|
-
<div class="lui:fixed lui:inset-y-0 lui:left-0 <%= sidebar_width_class %> lui:bg-surface-
|
|
67
|
+
<div class="lui:fixed lui:inset-y-0 lui:left-0 <%= sidebar_width_class %> lui:bg-surface-page lui:-translate-x-full lui:transition-transform lui:duration-300 lui:ease-out lui:shadow-xl lui:group-data-[open]:translate-x-0">
|
|
68
68
|
<nav class="lui:flex lui:h-full lui:min-h-0 lui:flex-col">
|
|
69
69
|
<%# Close button %>
|
|
70
70
|
<div class="lui:flex lui:items-center lui:justify-end lui:p-4">
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= content %>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class LightningUiKit::RadioGroup::OptionComponent < LightningUiKit::BaseComponent
|
|
2
|
+
attr_reader :value, :label, :description
|
|
3
|
+
|
|
4
|
+
def initialize(value:, label:, description: nil, disabled: false)
|
|
5
|
+
@value = value
|
|
6
|
+
@label = label
|
|
7
|
+
@description = description
|
|
8
|
+
@disabled = disabled
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def disabled?
|
|
12
|
+
@disabled
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<%= tag.div(class: classes, data: data, role: "radiogroup") do %>
|
|
2
|
+
<%= render_label %>
|
|
3
|
+
<% if @description %>
|
|
4
|
+
<%= tag.p(
|
|
5
|
+
@description,
|
|
6
|
+
class: "lui:text-base/6 lui:text-foreground-muted lui:data-disabled:opacity-50 lui:sm:text-sm/6",
|
|
7
|
+
data: description_data
|
|
8
|
+
) %>
|
|
9
|
+
<% end %>
|
|
10
|
+
<div data-slot="options" class="lui:space-y-3">
|
|
11
|
+
<% options.each_with_index do |option, index| %>
|
|
12
|
+
<div
|
|
13
|
+
role="radio"
|
|
14
|
+
tabindex="<%= index == 0 ? 0 : -1 %>"
|
|
15
|
+
aria-checked="<%= selected?(option.value) %>"
|
|
16
|
+
data-lui-radio-group-target="option"
|
|
17
|
+
data-value="<%= option.value %>"
|
|
18
|
+
data-action="click->lui-radio-group#select keydown->lui-radio-group#keydown"
|
|
19
|
+
class="lui:group lui:flex lui:items-start lui:gap-4 lui:cursor-pointer lui:focus:outline-hidden lui:sm:gap-3"
|
|
20
|
+
<%= "data-checked" if selected?(option.value) %>
|
|
21
|
+
<%= "data-disabled" if @disabled || option.disabled? %>
|
|
22
|
+
>
|
|
23
|
+
<span class="lui:relative lui:isolate lui:mt-0.5 lui:flex lui:size-[1.125rem] lui:shrink-0 lui:items-center lui:justify-center lui:rounded-full lui:sm:size-4
|
|
24
|
+
lui:border lui:border-border-strong
|
|
25
|
+
lui:before:absolute lui:before:inset-0 lui:before:-z-10 lui:before:rounded-full lui:before:bg-surface-input lui:before:shadow-sm
|
|
26
|
+
lui:after:absolute lui:after:inset-0 lui:after:rounded-full lui:after:shadow-[inset_0_1px_--theme(--color-white/15%)]
|
|
27
|
+
lui:group-data-checked:border-transparent lui:group-data-checked:bg-(--radio-checked-border) lui:group-data-checked:before:bg-(--radio-checked-bg)
|
|
28
|
+
lui:group-hover:border-border-hover lui:group-hover:group-data-checked:border-transparent
|
|
29
|
+
lui:group-data-focus:outline lui:group-data-focus:outline-2 lui:group-data-focus:outline-offset-2 lui:group-data-focus:outline-focus
|
|
30
|
+
lui:group-data-disabled:opacity-50 lui:group-data-disabled:border-border-strong lui:group-data-disabled:bg-surface-hover
|
|
31
|
+
lui:group-data-disabled:[--radio-indicator:var(--lui-theme-foreground)]/50 lui:group-data-disabled:before:bg-transparent
|
|
32
|
+
lui:forced-colors:[--radio-indicator:HighlightText] lui:forced-colors:[--radio-checked-bg:Highlight]
|
|
33
|
+
lui:forced-colors:group-data-disabled:[--radio-indicator:Highlight]
|
|
34
|
+
lui:[--radio-checked-bg:var(--lui-theme-surface-invert)] lui:[--radio-checked-border:var(--lui-theme-border-invert)] lui:[--radio-indicator:var(--lui-theme-foreground-invert)]
|
|
35
|
+
lui:transition-colors lui:duration-150"
|
|
36
|
+
>
|
|
37
|
+
<span class="lui:size-2 lui:rounded-full lui:bg-(--radio-indicator) lui:opacity-0 lui:group-data-checked:opacity-100 lui:transition-opacity lui:duration-150"></span>
|
|
38
|
+
</span>
|
|
39
|
+
<div class="lui:flex lui:flex-col lui:gap-0.5">
|
|
40
|
+
<span class="lui:text-base/6 lui:text-foreground lui:select-none lui:group-data-disabled:opacity-50 lui:sm:text-sm/6 lui:font-medium" data-slot="label">
|
|
41
|
+
<%= option.label %>
|
|
42
|
+
</span>
|
|
43
|
+
<% if option.description %>
|
|
44
|
+
<span class="lui:text-sm/5 lui:text-foreground-muted lui:group-data-disabled:opacity-50">
|
|
45
|
+
<%= option.description %>
|
|
46
|
+
</span>
|
|
47
|
+
<% end %>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
<% end %>
|
|
51
|
+
</div>
|
|
52
|
+
<%= render_hidden_field %>
|
|
53
|
+
<% if has_errors? %>
|
|
54
|
+
<%= tag.p(
|
|
55
|
+
error_messages,
|
|
56
|
+
class: "lui:text-base/6 lui:text-destructive-text lui:data-disabled:opacity-50 lui:sm:text-sm/6",
|
|
57
|
+
data: error_data
|
|
58
|
+
) %>
|
|
59
|
+
<% end %>
|
|
60
|
+
<% end %>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
class LightningUiKit::RadioGroupComponent < LightningUiKit::BaseComponent
|
|
2
|
+
include LightningUiKit::Errors
|
|
3
|
+
include LightningUiKit::Labelable
|
|
4
|
+
|
|
5
|
+
renders_many :options, LightningUiKit::RadioGroup::OptionComponent
|
|
6
|
+
|
|
7
|
+
def initialize(name:, label: nil, form: nil, error: nil, description: nil, selected: nil, disabled: false, **options)
|
|
8
|
+
@name = name
|
|
9
|
+
@label = label
|
|
10
|
+
@form = form
|
|
11
|
+
@error = error
|
|
12
|
+
@description = description
|
|
13
|
+
@selected = selected
|
|
14
|
+
@disabled = disabled
|
|
15
|
+
@options = options
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def classes
|
|
19
|
+
merge_classes([
|
|
20
|
+
"lui:[&>[data-slot=label]+[data-slot=description]]:mt-1 lui:[&>[data-slot=label]+[data-slot=options]]:mt-3 lui:[&>[data-slot=description]+[data-slot=options]]:mt-3 lui:[&>[data-slot=options]+[data-slot=error]]:mt-3 lui:*:data-[slot=label]:font-medium",
|
|
21
|
+
@options[:class]
|
|
22
|
+
].compact.join(" "))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def data
|
|
26
|
+
{
|
|
27
|
+
controller: "lui-radio-group"
|
|
28
|
+
}.merge(@options[:data] || {})
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def label_data
|
|
32
|
+
{slot: "label"}.tap do |data|
|
|
33
|
+
data[:disabled] = "true" if @disabled
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def description_data
|
|
38
|
+
{slot: "description"}.tap do |data|
|
|
39
|
+
data[:disabled] = "true" if @disabled
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def error_data
|
|
44
|
+
{slot: "error"}.tap do |data|
|
|
45
|
+
data[:disabled] = "true" if @disabled
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def render_label
|
|
50
|
+
return unless render_label?
|
|
51
|
+
|
|
52
|
+
tag.label(
|
|
53
|
+
effective_label,
|
|
54
|
+
class: "lui:text-base/6 lui:text-foreground lui:select-none lui:data-disabled:opacity-50 lui:sm:text-sm/6",
|
|
55
|
+
data: label_data
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def render_hidden_field
|
|
60
|
+
if @form
|
|
61
|
+
@form.hidden_field(@name, value: @selected, data: {lui_radio_group_target: "field"})
|
|
62
|
+
else
|
|
63
|
+
helpers.hidden_field_tag(@name, @selected, data: {lui_radio_group_target: "field"})
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def selected?(value)
|
|
68
|
+
@selected.to_s == value.to_s
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= content %>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<%= tag.div(class: classes, data: data) do %>
|
|
2
|
+
<div role="tablist" class="<%= list_classes %>">
|
|
3
|
+
<% tabs.each_with_index do |tab, index| %>
|
|
4
|
+
<button
|
|
5
|
+
type="button"
|
|
6
|
+
role="tab"
|
|
7
|
+
id="<%= tab_id(index) %>"
|
|
8
|
+
aria-controls="<%= panel_id(index) %>"
|
|
9
|
+
aria-selected="<%= index == @default_tab %>"
|
|
10
|
+
class="<%= tab_classes %>"
|
|
11
|
+
data-lui-tabs-target="tab"
|
|
12
|
+
data-action="click->lui-tabs#select"
|
|
13
|
+
>
|
|
14
|
+
<%= tab.title %>
|
|
15
|
+
</button>
|
|
16
|
+
<% end %>
|
|
17
|
+
</div>
|
|
18
|
+
<% tabs.each_with_index do |tab, index| %>
|
|
19
|
+
<div
|
|
20
|
+
role="tabpanel"
|
|
21
|
+
id="<%= panel_id(index) %>"
|
|
22
|
+
aria-labelledby="<%= tab_id(index) %>"
|
|
23
|
+
class="lui:flex-1 lui:outline-hidden lui:hidden"
|
|
24
|
+
tabindex="0"
|
|
25
|
+
data-lui-tabs-target="panel"
|
|
26
|
+
>
|
|
27
|
+
<%= tab %>
|
|
28
|
+
</div>
|
|
29
|
+
<% end %>
|
|
30
|
+
<% end %>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
class LightningUiKit::TabsComponent < LightningUiKit::BaseComponent
|
|
2
|
+
renders_many :tabs, LightningUiKit::Tabs::TabComponent
|
|
3
|
+
|
|
4
|
+
VARIANTS = %i[default line].freeze
|
|
5
|
+
|
|
6
|
+
def initialize(default_tab: 0, variant: :default, **options)
|
|
7
|
+
@default_tab = default_tab
|
|
8
|
+
@variant = VARIANTS.include?(variant) ? variant : :default
|
|
9
|
+
@options = options
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def classes
|
|
13
|
+
merge_classes([
|
|
14
|
+
"lui:flex lui:flex-col lui:gap-2 lui:w-full",
|
|
15
|
+
@options[:class]
|
|
16
|
+
].compact.join(" "))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def data
|
|
20
|
+
{
|
|
21
|
+
controller: "lui-tabs",
|
|
22
|
+
lui_tabs_default_tab_value: @default_tab
|
|
23
|
+
}.merge(@options[:data] || {})
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def list_classes
|
|
27
|
+
base = "lui:inline-flex lui:w-fit lui:items-center lui:justify-center lui:text-foreground-muted"
|
|
28
|
+
case @variant
|
|
29
|
+
when :line
|
|
30
|
+
"#{base} lui:gap-1 lui:rounded-none lui:bg-transparent"
|
|
31
|
+
else
|
|
32
|
+
"#{base} lui:h-9 lui:rounded-lg lui:bg-surface-tertiary lui:p-[3px]"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def tab_classes
|
|
37
|
+
base = "lui:relative lui:inline-flex lui:items-center lui:justify-center lui:gap-1.5 lui:rounded-md lui:px-3 lui:py-1 lui:text-sm lui:font-medium lui:whitespace-nowrap lui:cursor-pointer " \
|
|
38
|
+
"lui:transition-all lui:duration-150 " \
|
|
39
|
+
"lui:text-foreground-muted lui:hover:text-foreground " \
|
|
40
|
+
"lui:focus:outline-hidden lui:focus-visible:outline lui:focus-visible:outline-2 lui:focus-visible:outline-offset-[-2px] lui:focus-visible:outline-focus " \
|
|
41
|
+
"lui:disabled:pointer-events-none lui:disabled:opacity-50"
|
|
42
|
+
|
|
43
|
+
case @variant
|
|
44
|
+
when :line
|
|
45
|
+
"#{base} lui:bg-transparent lui:data-[active]:bg-transparent " \
|
|
46
|
+
"lui:data-[active]:text-foreground " \
|
|
47
|
+
"lui:after:absolute lui:after:inset-x-0 lui:after:bottom-[-5px] lui:after:h-0.5 lui:after:bg-foreground lui:after:opacity-0 lui:after:transition-opacity lui:data-[active]:after:opacity-100"
|
|
48
|
+
else
|
|
49
|
+
"#{base} lui:h-[calc(100%-1px)] lui:border lui:border-transparent " \
|
|
50
|
+
"lui:data-[active]:bg-surface lui:data-[active]:text-foreground lui:data-[active]:border-border lui:data-[active]:shadow-sm"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def line_variant?
|
|
55
|
+
@variant == :line
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def tab_id(index)
|
|
59
|
+
"tab-#{object_id}-#{index}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def panel_id(index)
|
|
63
|
+
"panel-#{object_id}-#{index}"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["field", "option"];
|
|
5
|
+
|
|
6
|
+
select(event) {
|
|
7
|
+
const option = event.currentTarget;
|
|
8
|
+
if (option.dataset.disabled !== undefined) return;
|
|
9
|
+
|
|
10
|
+
this.activate(option);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
keydown(event) {
|
|
14
|
+
const currentIndex = this.optionTargets.indexOf(event.currentTarget);
|
|
15
|
+
let newIndex;
|
|
16
|
+
|
|
17
|
+
switch (event.key) {
|
|
18
|
+
case "ArrowDown":
|
|
19
|
+
case "ArrowRight":
|
|
20
|
+
event.preventDefault();
|
|
21
|
+
newIndex = this.nextEnabledIndex(currentIndex, 1);
|
|
22
|
+
break;
|
|
23
|
+
case "ArrowUp":
|
|
24
|
+
case "ArrowLeft":
|
|
25
|
+
event.preventDefault();
|
|
26
|
+
newIndex = this.nextEnabledIndex(currentIndex, -1);
|
|
27
|
+
break;
|
|
28
|
+
case " ":
|
|
29
|
+
case "Enter":
|
|
30
|
+
event.preventDefault();
|
|
31
|
+
this.activate(event.currentTarget);
|
|
32
|
+
return;
|
|
33
|
+
default:
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (newIndex !== -1) {
|
|
38
|
+
this.activate(this.optionTargets[newIndex]);
|
|
39
|
+
this.optionTargets[newIndex].focus();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
activate(option) {
|
|
44
|
+
const value = option.dataset.value;
|
|
45
|
+
|
|
46
|
+
this.optionTargets.forEach((opt) => {
|
|
47
|
+
if (opt === option) {
|
|
48
|
+
opt.dataset.checked = "";
|
|
49
|
+
opt.setAttribute("aria-checked", "true");
|
|
50
|
+
opt.setAttribute("tabindex", "0");
|
|
51
|
+
} else {
|
|
52
|
+
delete opt.dataset.checked;
|
|
53
|
+
opt.setAttribute("aria-checked", "false");
|
|
54
|
+
opt.setAttribute("tabindex", "-1");
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
this.fieldTarget.value = value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
nextEnabledIndex(currentIndex, direction) {
|
|
62
|
+
const length = this.optionTargets.length;
|
|
63
|
+
let index = currentIndex;
|
|
64
|
+
|
|
65
|
+
for (let i = 0; i < length; i++) {
|
|
66
|
+
index = (index + direction + length) % length;
|
|
67
|
+
if (this.optionTargets[index].dataset.disabled === undefined) {
|
|
68
|
+
return index;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return -1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["tab", "panel"];
|
|
5
|
+
static values = {
|
|
6
|
+
defaultTab: { type: Number, default: 0 }
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
connect() {
|
|
10
|
+
this.activate(this.defaultTabValue);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
select(event) {
|
|
14
|
+
event.preventDefault();
|
|
15
|
+
const index = this.tabTargets.indexOf(event.currentTarget);
|
|
16
|
+
if (index !== -1) {
|
|
17
|
+
this.activate(index);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
activate(index) {
|
|
22
|
+
this.tabTargets.forEach((tab, i) => {
|
|
23
|
+
if (i === index) {
|
|
24
|
+
tab.dataset.active = "";
|
|
25
|
+
tab.setAttribute("aria-selected", "true");
|
|
26
|
+
tab.setAttribute("tabindex", "0");
|
|
27
|
+
} else {
|
|
28
|
+
delete tab.dataset.active;
|
|
29
|
+
tab.setAttribute("aria-selected", "false");
|
|
30
|
+
tab.setAttribute("tabindex", "-1");
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
this.panelTargets.forEach((panel, i) => {
|
|
35
|
+
if (i === index) {
|
|
36
|
+
panel.classList.remove("lui:hidden");
|
|
37
|
+
} else {
|
|
38
|
+
panel.classList.add("lui:hidden");
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
tabTargetConnected() {
|
|
44
|
+
this.tabTargets.forEach((tab) => {
|
|
45
|
+
tab.addEventListener("keydown", this.handleKeydown.bind(this));
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
handleKeydown(event) {
|
|
50
|
+
const currentIndex = this.tabTargets.indexOf(event.currentTarget);
|
|
51
|
+
let newIndex;
|
|
52
|
+
|
|
53
|
+
switch (event.key) {
|
|
54
|
+
case "ArrowLeft":
|
|
55
|
+
event.preventDefault();
|
|
56
|
+
newIndex = currentIndex === 0 ? this.tabTargets.length - 1 : currentIndex - 1;
|
|
57
|
+
break;
|
|
58
|
+
case "ArrowRight":
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
newIndex = currentIndex === this.tabTargets.length - 1 ? 0 : currentIndex + 1;
|
|
61
|
+
break;
|
|
62
|
+
case "Home":
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
newIndex = 0;
|
|
65
|
+
break;
|
|
66
|
+
case "End":
|
|
67
|
+
event.preventDefault();
|
|
68
|
+
newIndex = this.tabTargets.length - 1;
|
|
69
|
+
break;
|
|
70
|
+
default:
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.activate(newIndex);
|
|
75
|
+
this.tabTargets[newIndex].focus();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -10,7 +10,7 @@ window.Stimulus = application
|
|
|
10
10
|
|
|
11
11
|
import ClipboardController from './controllers/clipboard_controller'
|
|
12
12
|
import CheckboxController from './controllers/checkbox_controller'
|
|
13
|
-
import
|
|
13
|
+
import AlertController from './controllers/alert_controller'
|
|
14
14
|
import LayoutController from './controllers/layout_controller'
|
|
15
15
|
import MainController from './controllers/main_controller'
|
|
16
16
|
import AccordionController from './controllers/accordion_controller'
|
|
@@ -23,11 +23,13 @@ import ToastController from './controllers/toast_controller'
|
|
|
23
23
|
import TooltipController from './controllers/tooltip_controller'
|
|
24
24
|
import ComboboxController from './controllers/combobox_controller'
|
|
25
25
|
import FieldController from './controllers/field_controller'
|
|
26
|
+
import TabsController from './controllers/tabs_controller'
|
|
27
|
+
import RadioGroupController from './controllers/radio_group_controller'
|
|
26
28
|
|
|
27
29
|
export function registerLuiControllers(application) {
|
|
28
30
|
application.register(`${namespace}-clipboard`, ClipboardController)
|
|
29
31
|
application.register(`${namespace}-checkbox`, CheckboxController)
|
|
30
|
-
application.register(`${namespace}-
|
|
32
|
+
application.register(`${namespace}-alert`, AlertController)
|
|
31
33
|
application.register(`${namespace}-layout`, LayoutController)
|
|
32
34
|
application.register(`${namespace}-main`, MainController)
|
|
33
35
|
application.register(`${namespace}-accordion`, AccordionController)
|
|
@@ -40,6 +42,8 @@ export function registerLuiControllers(application) {
|
|
|
40
42
|
application.register(`${namespace}-tooltip`, TooltipController)
|
|
41
43
|
application.register(`${namespace}-combobox`, ComboboxController)
|
|
42
44
|
application.register(`${namespace}-field`, FieldController)
|
|
45
|
+
application.register(`${namespace}-tabs`, TabsController)
|
|
46
|
+
application.register(`${namespace}-radio-group`, RadioGroupController)
|
|
43
47
|
}
|
|
44
48
|
registerLuiControllers(application)
|
|
45
49
|
|