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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/app/assets/stylesheets/lightning_ui_kit/application.css +1 -0
  4. data/app/assets/stylesheets/lightning_ui_kit/themes.css +15 -2
  5. data/app/assets/vendor/lightning_ui_kit.css +404 -68
  6. data/app/assets/vendor/lightning_ui_kit.js +3 -3
  7. data/app/components/lightning_ui_kit/accordion/item_component.html.erb +30 -0
  8. data/app/components/lightning_ui_kit/accordion/item_component.rb +13 -0
  9. data/app/components/lightning_ui_kit/accordion_component.html.erb +5 -0
  10. data/app/components/lightning_ui_kit/accordion_component.rb +22 -0
  11. data/app/components/lightning_ui_kit/alert_component.html.erb +14 -3
  12. data/app/components/lightning_ui_kit/alert_component.rb +58 -5
  13. data/app/components/lightning_ui_kit/card_component.html.erb +34 -0
  14. data/app/components/lightning_ui_kit/card_component.rb +22 -0
  15. data/app/components/lightning_ui_kit/layout_component.html.erb +3 -3
  16. data/app/components/lightning_ui_kit/radio_group/option_component.html.erb +1 -0
  17. data/app/components/lightning_ui_kit/radio_group/option_component.rb +14 -0
  18. data/app/components/lightning_ui_kit/radio_group_component.html.erb +60 -0
  19. data/app/components/lightning_ui_kit/radio_group_component.rb +70 -0
  20. data/app/components/lightning_ui_kit/tabs/tab_component.html.erb +1 -0
  21. data/app/components/lightning_ui_kit/tabs/tab_component.rb +8 -0
  22. data/app/components/lightning_ui_kit/tabs_component.html.erb +30 -0
  23. data/app/components/lightning_ui_kit/tabs_component.rb +65 -0
  24. data/app/javascript/lightning_ui_kit/controllers/radio_group_controller.js +74 -0
  25. data/app/javascript/lightning_ui_kit/controllers/tabs_controller.js +77 -0
  26. data/app/javascript/lightning_ui_kit/index.js +6 -2
  27. data/lib/lightning_ui_kit/builder.rb +16 -4
  28. data/lib/lightning_ui_kit/version.rb +1 -1
  29. metadata +18 -4
  30. data/app/components/lightning_ui_kit/banner_component.html.erb +0 -17
  31. data/app/components/lightning_ui_kit/banner_component.rb +0 -33
  32. /data/app/javascript/lightning_ui_kit/controllers/{banner_controller.js → alert_controller.js} +0 -0
@@ -0,0 +1,5 @@
1
+ <%= tag.div(class: classes, data: data) do %>
2
+ <% items.each do |item| %>
3
+ <%= item %>
4
+ <% end %>
5
+ <% end %>
@@ -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 class: classes do %>
2
- <%= heroicon("information-circle", options: { class: "lui:w-4 lui:h-4 lui:me-3 lui:shrink-0 lui:inline" }) %>
3
- <%= content %>
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
- def initialize(type: :info, **options)
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 default_classes
8
- "lui:flex lui:items-center lui:p-4 lui:text-sm lui:text-neutral-text lui:rounded-lg lui:bg-neutral-bg"
10
+ def classes
11
+ merge_classes([base_classes, type_classes, @options[:class]].compact.join(" "))
9
12
  end
10
13
 
11
- def classes
12
- [default_classes, @options[:class]].compact.join(" ")
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:lg:bg-surface-aside lui:overflow-hidden" data-controller="lui-layout">
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-aside lui:-translate-x-full lui:transition-transform lui:duration-300 lui:ease-out lui:shadow-xl lui:group-data-[open]:translate-x-0">
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,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,8 @@
1
+ class LightningUiKit::Tabs::TabComponent < LightningUiKit::BaseComponent
2
+ attr_reader :title
3
+
4
+ def initialize(title:, **options)
5
+ @title = title
6
+ @options = options
7
+ end
8
+ end
@@ -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 BannerController from './controllers/banner_controller'
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}-banner`, BannerController)
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