bulma-phlex 0.17.0 → 0.18.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6a4e33701b1ed15897b743789cc3fb1923fdaf70fd065035f8342836058927ed
4
- data.tar.gz: 772252cf5d5660a0a926f4d24a2ffd73898947a6db4ce3f8ea880978acdda0bf
3
+ metadata.gz: d62a43330834f293718993ab3227796dbb76d3ae57b4c0efca10df5dc1e35a6b
4
+ data.tar.gz: 1b310813d75fe054a993a02d258a05fa52ba4f15db249d2e269c1871fdf2a025
5
5
  SHA512:
6
- metadata.gz: 7ec1799752698497e06b075b7378d18a3b820e80ec9e95c7448eb1999ac93514c785d6a61f95434ba02962edb103aa398bd6994506a0954804af52becb56f9ac
7
- data.tar.gz: 4071dd6000764651b7a677d1ab6ac94c15b59043692602be997924ec094977f41eaf704222554e89ce33f0a49feadc65eeb680ef8c3d36963034995c7b867570
6
+ metadata.gz: a0b4c95ba751e8a2f5718b08bcd389ebe055f087271a1ee77f06c2e9292088bc4a770aeeacdb31c304349ac372373cd40ae5fd90f498eef15a1c6502732028cc
7
+ data.tar.gz: 2579cf14a6d59153dc4e44781eba67b451513e50d551e59141be6f61be9d130862c6553643b150393941a40f7ea99f10f40ff10dd824489ea12d18afdaeff875
data/README.md CHANGED
@@ -9,6 +9,7 @@ This gem provides a set of ready-to-use [Phlex](https://github.com/phlex-ruby/ph
9
9
  ## Table of Contents
10
10
 
11
11
  - [Installation](#installation)
12
+ - [Configuration](#configuration)
12
13
  - [Usage](#usage)
13
14
  - [Breadcrumb](#breadcrumb)
14
15
  - [Button](#button)
@@ -23,6 +24,7 @@ This gem provides a set of ready-to-use [Phlex](https://github.com/phlex-ruby/ph
23
24
  - [Image](#image)
24
25
  - [Level](#level)
25
26
  - [Media Object](#media-object)
27
+ - [Menu](#menu)
26
28
  - [Message](#message)
27
29
  - [Modal](#modal)
28
30
  - [NavigationBar](#navigationbar)
@@ -76,6 +78,25 @@ This gem requires:
76
78
  require "bulma-phlex"
77
79
  ```
78
80
 
81
+ ## Configuration
82
+
83
+ Icons chosen internally by components can be configured globally. The default icons are Font Awesome 7 solid icons, but you can change them to other icons by providing the appropriate class names.
84
+
85
+ ```ruby
86
+ BulmaPhlex.configure do |config|
87
+ config.icons.sort = {
88
+ ascending: "fa-solid fa-arrow-up",
89
+ descending: "fa-solid fa-arrow-down",
90
+ inactive: "fa-solid fa-sort"
91
+ }
92
+ config.icons.dropdown = "fa-solid fa-chevron-down"
93
+ config.icons.file_upload = "fa-solid fa-upload"
94
+ config.icons.conditional = "fa-solid fa-check"
95
+ end
96
+ ```
97
+
98
+ Sortable columns also accept an `icons:` hash for a per-column override. Dropdowns accept `icon:` and conditional table columns accept `icon_class:`.
99
+
79
100
  ## Usage
80
101
 
81
102
  Use the Phlex components in your Rails views or any Ruby application that supports Phlex components.
@@ -101,7 +122,7 @@ Renders a [Bulma button](https://bulma.io/documentation/elements/button/) elemen
101
122
  The component generates a `<button>` by default. Pass an `href:` attribute to generate an `<a>` element instead. Pass `input: "submit"` (or `"button"` or `"reset"`) to generate an `<input>` element.
102
123
 
103
124
  ```ruby
104
- BulmaPhlex::Button("Like", color: "primary", size: "large", icon: "fas fa-thumbs-up")
125
+ BulmaPhlex::Button("Like", color: "primary", size: "large", icon: "fa-solid fa-thumbs-up")
105
126
  BulmaPhlex::Button(href: "/profile") { "View Profile" }
106
127
  BulmaPhlex::Button(input: "submit", color: "success")
107
128
  ```
@@ -120,7 +141,7 @@ render BulmaPhlex::Card.new do |card|
120
141
  "This is some card content"
121
142
  end
122
143
  card.footer_link("View", "/view", target: "_blank")
123
- card.footer_link("Edit", "/edit", icon: "fas fa-edit")
144
+ card.footer_link("Edit", "/edit", icon: "fa-solid fa-pen-to-square")
124
145
  end
125
146
  ```
126
147
 
@@ -222,9 +243,12 @@ end
222
243
 
223
244
  Renders a [Bulma icon](https://bulma.io/documentation/elements/icon/) element. Supports color, size, optional text alongside the icon, and left/right positioning for use inside form controls.
224
245
 
246
+ When text is rendered alongside the icon, Bulma allows the text and icon to wrap by default. Pass `nowrap: true` when they form a compact UI unit that should stay together, such as a sortable table heading, toolbar action, button label, or navigation item. Leave it unset when the text should be allowed to wrap in a constrained container.
247
+
225
248
  ```ruby
226
- render BulmaPhlex::Icon.new("fas fa-user")
227
- render BulmaPhlex::Icon.new("fas fa-home", size: :large, color: :primary, text_right: "Home")
249
+ render BulmaPhlex::Icon.new("fa-solid fa-user")
250
+ render BulmaPhlex::Icon.new("fa-solid fa-home", size: :large, color: :primary, text_right: "Home")
251
+ render BulmaPhlex::Icon.new("fa-solid fa-sort", text_left: "Revenue", nowrap: true)
228
252
  ```
229
253
 
230
254
  ### Image
@@ -275,6 +299,45 @@ render BulmaPhlex::MediaObject.new do |media|
275
299
  end
276
300
  ```
277
301
 
302
+ ### Menu
303
+
304
+ Renders a [Bulma menu](https://bulma.io/documentation/components/menu/) with support for labels, lists, and nested lists.
305
+
306
+ ```ruby
307
+ render BulmaPhlex::Menu.new do |menu|
308
+ menu.label("General")
309
+ menu.list do
310
+ menu.item("Dashboard", href: "/dashboard", active: true)
311
+ menu.item("Customers", href: "/customers")
312
+ end
313
+
314
+ menu.label("Administration")
315
+ menu.list do
316
+ menu.item("Team Settings", href: "/teams/settings")
317
+ menu.item("Manage Your Team", href: "/teams/dashboard") do
318
+ menu.item("Members", href: "/members")
319
+ menu.item("Plugins", href: "/plugins")
320
+ menu.item("Add a member", href: "/add-member")
321
+ end
322
+ end
323
+ end
324
+ ```
325
+
326
+ Nested lists can be created by passing a block to the `item` method. An expandable section (header opens and closes but is not a link) can be created with the `expandable_item` method.
327
+
328
+ ```ruby
329
+ render BulmaPhlex::Menu.new do |menu|
330
+ menu.label("General")
331
+ menu.list do
332
+ menu.item("Dashboard", href: "/dashboard", active: true)
333
+ menu.expandable_item("Customers") do
334
+ menu.item("Active Customers", href: "/customers/active")
335
+ menu.item("Inactive Customers", href: "/customers/inactive")
336
+ end
337
+ end
338
+ end
339
+ ```
340
+
278
341
  ### Message
279
342
 
280
343
  Renders a [Bulma message](https://bulma.io/documentation/components/message/) component with a header, optional delete button, and body.
@@ -377,7 +440,32 @@ end
377
440
  In addition to `column`, two specialized column methods are available:
378
441
 
379
442
  - `date_column(header, format: "%Y-%m-%d")` — formats the value with `strftime`
380
- - `conditional_icon(header, icon_class: "fas fa-check")` — shows an icon when the block returns truthy
443
+ - `conditional_icon(header, icon_class:)` — shows the configured conditional icon, or the supplied icon, when the block returns truthy
444
+
445
+ #### Sortable columns
446
+
447
+ Pass a sort hash with the URL for the next sort state to add a sortable header. The component renders the sort link, icon, and accessible sort state; your application remains responsible for interpreting the sort parameters and ordering the records.
448
+
449
+ ```ruby
450
+ render BulmaPhlex::Table.new(@users) do |table|
451
+ table.column(
452
+ "Name",
453
+ sort: { href: users_path(sort: "name") }
454
+ ) { |user| user.full_name }
455
+
456
+ table.date_column(
457
+ "Joined",
458
+ sort: {
459
+ href: users_path(sort: "created_at"),
460
+ current_direction: :descending
461
+ }
462
+ ) { |user| user.created_at }
463
+ end
464
+ ```
465
+
466
+ Set `current_direction:` to `:ascending` or `:descending` for the currently active column. Pass additional attributes for the generated link with `link_attributes:`. The component generates an accessible label from the column header and sort state.
467
+
468
+ #### Pagination
381
469
 
382
470
  To add pagination to the table, call `paginate` with a block that returns a path given a page number:
383
471
 
@@ -385,6 +473,8 @@ To add pagination to the table, call `paginate` with a block that returns a path
385
473
  table.paginate { |page| products_path(page: page) }
386
474
  ```
387
475
 
476
+ #### Custom row attributes
477
+
388
478
  Pass HTML attributes to the `tr` elements via the `row` method, using either keyword arguments or a block
389
479
  that receives the record for the row:
390
480
 
@@ -392,6 +482,8 @@ that receives the record for the row:
392
482
  table.row(class: "custom-row-class") { |row| { id: "row-id-#{row.id}" } }
393
483
  ```
394
484
 
485
+ #### Responsive columns
486
+
395
487
  Hide columns on smaller screens with the column `hidden` option:
396
488
 
397
489
  ```ruby
@@ -405,7 +497,7 @@ Renders a [Bulma tabs](https://bulma.io/documentation/components/tabs/) componen
405
497
  ```ruby
406
498
  render BulmaPhlex::Tabs.new(boxed: true) do |tabs|
407
499
  tabs.tab(id: "profile", title: "Profile", active: true) { "Profile content" }
408
- tabs.tab(id: "settings", title: "Settings", icon: "fas fa-cog") { "Settings content" }
500
+ tabs.tab(id: "settings", title: "Settings", icon: "fa-solid fa-gear") { "Settings content" }
409
501
  end
410
502
  ```
411
503
 
@@ -80,7 +80,7 @@ module BulmaPhlex
80
80
  #
81
81
  # - `text` — The link label text
82
82
  # - `href` — The URL the link points to
83
- # - `icon:` — Optional icon class string (e.g. `"fas fa-edit"`) to render an icon alongside the text
83
+ # - `icon:` — Optional icon class string (e.g. `"fa-solid fa-pen-to-square"`) to render an icon alongside the text
84
84
  # - `**html_attributes` — Additional HTML attributes for the `<a>` element (e.g. `target:`, `class:`)
85
85
  def footer_link(text, href, **html_attributes)
86
86
  (@footer_items ||= []) << [text, href, html_attributes]
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BulmaPhlex
4
+ # Stores application-wide defaults for component-owned icons.
5
+ class Configuration
6
+ # Stores the icon classes used by components when no local override is supplied.
7
+ class Icons
8
+ attr_accessor :sort, :dropdown, :file_upload, :conditional
9
+
10
+ def initialize
11
+ @sort = {
12
+ ascending: "fa-solid fa-sort-up",
13
+ descending: "fa-solid fa-sort-down",
14
+ inactive: "fa-solid fa-sort"
15
+ }
16
+ @dropdown = "fa-solid fa-angle-down"
17
+ @file_upload = "fa-solid fa-upload"
18
+ @conditional = "fa-solid fa-check"
19
+ end
20
+ end
21
+
22
+ attr_reader :icons
23
+
24
+ def initialize
25
+ @icons = Icons.new
26
+ end
27
+ end
28
+ end
@@ -23,19 +23,19 @@ module BulmaPhlex
23
23
  # - `label` — The text displayed in the dropdown trigger button
24
24
  # - `click` — Stimulus controller name for toggling; set to `false` for hover mode instead
25
25
  # - `alignment` — Alignment of the dropdown menu: `"left"` (default), `"right"`, or `"up"`
26
- # - `icon` — Icon class for the trigger button (default: `"fas fa-angle-down"`)
26
+ # - `icon` — Icon class for the trigger button (default: configured dropdown icon)
27
27
  # - `**html_attributes` — Additional HTML attributes for the outermost dropdown element
28
- def self.new(label, click: "bulma-phlex--dropdown", alignment: "left", icon: "fas fa-angle-down",
28
+ def self.new(label, click: "bulma-phlex--dropdown", alignment: "left", icon: nil,
29
29
  **html_attributes)
30
30
  super
31
31
  end
32
32
 
33
- def initialize(label, click: "bulma-phlex--dropdown", alignment: "left", icon: "fas fa-angle-down",
33
+ def initialize(label, click: "bulma-phlex--dropdown", alignment: "left", icon: nil,
34
34
  **html_attributes)
35
35
  @label = label
36
36
  @click = click
37
37
  @alignment = alignment
38
- @icon = icon
38
+ @icon = icon || BulmaPhlex.config.icons.dropdown
39
39
  @html_attributes = html_attributes
40
40
  end
41
41
 
@@ -25,6 +25,7 @@ module BulmaPhlex
25
25
  # - `align` — Aligns the file input: `"right"` or `"centered"`
26
26
  # - `fullwidth` — If `true`, makes the file input full width
27
27
  # - `boxed` — If `true`, makes the file input boxed
28
+ # - `icon` — Icon class for the upload control (default: configured file upload icon)
28
29
  # - `data_attributes_builder` — A custom builder for the data attributes used for Stimulus integration;
29
30
  # defaults to a builder with the controller name `"bulma-phlex--file-input-display"`
30
31
  def self.new(color: nil,
@@ -33,6 +34,7 @@ module BulmaPhlex
33
34
  align: nil,
34
35
  fullwidth: false,
35
36
  boxed: false,
37
+ icon: nil,
36
38
  data_attributes_builder: nil)
37
39
  super
38
40
  end
@@ -43,6 +45,7 @@ module BulmaPhlex
43
45
  align: nil,
44
46
  fullwidth: false,
45
47
  boxed: false,
48
+ icon: nil,
46
49
  data_attributes_builder: nil)
47
50
  @color = color
48
51
  @size = size
@@ -50,6 +53,7 @@ module BulmaPhlex
50
53
  @align = align
51
54
  @fullwidth = fullwidth
52
55
  @boxed = boxed
56
+ @icon = icon || BulmaPhlex.config.icons.file_upload
53
57
 
54
58
  return unless @name
55
59
 
@@ -63,7 +67,7 @@ module BulmaPhlex
63
67
  yield(@data_attributes_builder&.for_file_input)
64
68
  span(class: "file-cta") do
65
69
  span(class: "file-icon") do
66
- i(class: "fas fa-upload")
70
+ i(class: @icon)
67
71
  end
68
72
  span(class: "file-label") { plain " Choose a file… " }
69
73
  end
@@ -6,8 +6,8 @@ module BulmaPhlex
6
6
  class FormControl < Base
7
7
  # **Parameters**
8
8
  #
9
- # - `icon_left` — Icon class for the left side of the control (e.g. `"fas fa-check"`)
10
- # - `icon_right` — Icon class for the right side of the control (e.g. `"fas fa-check"`)
9
+ # - `icon_left` — Icon class for the left side of the control (e.g. `"fa-solid fa-check"`)
10
+ # - `icon_right` — Icon class for the right side of the control (e.g. `"fa-solid fa-check"`)
11
11
  # - `**html_attributes` — Additional HTML attributes for the control element
12
12
  def self.new(icon_left: nil, icon_right: nil, **html_attributes)
13
13
  super
@@ -21,8 +21,8 @@ module BulmaPhlex
21
21
  # **Parameters**
22
22
  #
23
23
  # - `help` — Optional help text displayed below the input
24
- # - `icon_left` — Icon class for an icon to the left of the input (e.g. `"fas fa-user"`)
25
- # - `icon_right` — Icon class for an icon to the right of the input (e.g. `"fas fa-check"`)
24
+ # - `icon_left` — Icon class for an icon to the left of the input (e.g. `"fa-solid fa-user"`)
25
+ # - `icon_right` — Icon class for an icon to the right of the input (e.g. `"fa-solid fa-check"`)
26
26
  # - `column` — If `true`, makes the field a column; a size string (e.g. `"half"`) sets the size for
27
27
  # all breakpoints; a hash (e.g. `{ mobile: "full", desktop: "half" }`) sets responsive sizes
28
28
  # - `grid` — If `true`, makes the field a grid cell; a size string sets the cell size
@@ -5,27 +5,31 @@ module BulmaPhlex
5
5
  # form control positioning.
6
6
  #
7
7
  # Supports **color** and **size** options, optional **text** to the left or right of the icon,
8
- # and **positioning** helpers (`left`/`right`) for use inside form controls. When text is provide,
8
+ # and **positioning** helpers (`left`/`right`) for use inside form controls. When text is provided,
9
9
  # the icon and text are wrapped in a container with the `icon-text` class for proper spacing, and
10
- # the icon includes `aria-hidden="true"` for accessibility.
10
+ # the icon includes `aria-hidden="true"` for accessibility. Use `nowrap: true` when the icon and
11
+ # text form a compact unit that should remain on one line, such as a sortable table heading or
12
+ # toolbar action; leave it unset when text should be allowed to wrap.
11
13
  #
12
14
  # Additional HTML attributes can be passed to the icon's `<span>` element via `**html_attributes`,
13
15
  # and to the inner `<i>` by nested them under `icon_attributes`.
14
16
  #
15
17
  # ## Example
16
18
  #
17
- # render BulmaPhlex::Icon.new("fas fa-home")
18
- # render BulmaPhlex::Icon.new("fas fa-home", color: :primary, size: :large, text_right: "Home")
19
+ # render BulmaPhlex::Icon.new("fa-solid fa-home")
20
+ # render BulmaPhlex::Icon.new("fa-solid fa-home", color: :primary, size: :large, text_right: "Home")
19
21
  class Icon < BulmaPhlex::Base
20
22
  # **Parameters**
21
23
  #
22
- # - `icon` — The icon class string (e.g. `"fas fa-home"`)
24
+ # - `icon` — The icon class string (e.g. `"fa-solid fa-home"`)
23
25
  # - `color` — Sets the [color of the icon](https://bulma.io/documentation/elements/icon/#colors)
24
26
  # - `size` — Sets the [size of the icon](https://bulma.io/documentation/elements/icon/#sizes)
25
27
  # - `text_right` — Text to display to the right of the icon
26
28
  # - `text_left` — Text to display to the left of the icon
27
29
  # - `left` — If `true`, adds the `is-left` class for use in form controls
28
30
  # - `right` — If `true`, adds the `is-right` class for use in form controls
31
+ # - `nowrap` — If `true`, prevents text and the icon from wrapping onto separate lines. Use for
32
+ # compact icon-label units; leave unset when the text should be allowed to wrap.
29
33
  # - `**html_attributes` — Additional HTML attributes for the icon span element. Nest attributes under
30
34
  # `icon_attributes` to apply them to the inner `<i>` element instead.
31
35
  def self.new(icon,
@@ -35,6 +39,7 @@ module BulmaPhlex
35
39
  color: nil,
36
40
  left: false,
37
41
  right: false,
42
+ nowrap: false,
38
43
  **html_attributes)
39
44
  super
40
45
  end
@@ -46,6 +51,7 @@ module BulmaPhlex
46
51
  color: nil,
47
52
  left: false,
48
53
  right: false,
54
+ nowrap: false,
49
55
  **html_attributes)
50
56
  @icon = icon
51
57
  @text_right = text_right
@@ -54,6 +60,7 @@ module BulmaPhlex
54
60
  @color = color
55
61
  @left = left
56
62
  @right = right
63
+ @nowrap = nowrap
57
64
  @html_attributes = html_attributes.clone
58
65
  @icon_attributes = @html_attributes.delete(:icon_attributes) || {}
59
66
  end
@@ -73,12 +80,18 @@ module BulmaPhlex
73
80
  def optional_text_wrapper(&)
74
81
  if @text_right || @text_left
75
82
  add_aria_hidden_to_icon_attributes
76
- span(class: "icon-text", &)
83
+ span(class: icon_text_classes, &)
77
84
  else
78
85
  yield
79
86
  end
80
87
  end
81
88
 
89
+ def icon_text_classes
90
+ classes = ["icon-text"]
91
+ classes << "is-flex-wrap-nowrap" if @nowrap
92
+ classes.join(" ")
93
+ end
94
+
82
95
  def add_aria_hidden_to_icon_attributes
83
96
  @icon_attributes = mix({ aria: { hidden: "true" } }, @icon_attributes)
84
97
  end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BulmaPhlex
4
+ # Renders a [Bulma menu](https://bulma.io/documentation/components/menu/) component with a label and list of items.
5
+ #
6
+ # The menu supports headers with the `label` method, lists with the `list` method, and items with the `item` method.
7
+ # If an item has nested items, use a block within the `item` method to add the nested items. An item can be marked as
8
+ # active by passing `active: true` to the `item` method.
9
+ #
10
+ # Additional HTML attributes can be passed to the constructor as well as any of the methods.
11
+ #
12
+ # ## Example
13
+ #
14
+ # render BulmaPhlex::Menu.new do |menu|
15
+ # menu.label "General"
16
+ # menu.list do |list|
17
+ # list.item "Dashboard", href: "#dashboard"
18
+ # list.item "Customers", href: "#customers" do |nested|
19
+ # nested.item "Active Customers", href: "#active-customers"
20
+ # nested.item "Inactive Customers", href: "#inactive-customers"
21
+ # end
22
+ # end
23
+ # end
24
+ class Menu < BulmaPhlex::Base
25
+ # **Parameters**
26
+ #
27
+ # - `**html_attributes` — Additional HTML attributes for the menu element
28
+ def self.new(**html_attributes)
29
+ super
30
+ end
31
+
32
+ def initialize(**html_attributes)
33
+ @html_attributes = html_attributes
34
+ end
35
+
36
+ def view_template(&)
37
+ aside(**mix({ class: "menu" }, @html_attributes)) do
38
+ yield(self) if block_given?
39
+ end
40
+ end
41
+
42
+ # Adds a header label to the menu. Additional HTML attributes can be passed to customize the label element.
43
+ def label(text, **attributes)
44
+ p(**mix({ class: "menu-label" }, attributes)) { text }
45
+ end
46
+
47
+ # Pass a block to add a list of items to the menu. Additional HTML attributes can be passed to customize the
48
+ # list element.
49
+ def list(**attributes, &)
50
+ ul(**mix({ class: "menu-list" }, attributes)) do
51
+ yield(self) if block_given?
52
+ end
53
+ end
54
+
55
+ # Adds an item to the menu list. The `href` parameter is required to specify the link for the item. If the item is
56
+ # active, pass `active: true`. Additional HTML attributes can be passed to customize the link element. If the item
57
+ # has nested items, pass a block to add the nested items.
58
+ def item(label, href:, active: false, **attributes, &)
59
+ li_attributes, attributes = parse_li_attributes(attributes)
60
+
61
+ li(**li_attributes) do
62
+ attributes = mix({ class: "is-active" }, attributes) if active
63
+ a(**mix({ href: }, attributes)) { label }
64
+ nested_list(&) if block_given?
65
+ end
66
+ end
67
+
68
+ def expandable_item(label, open: false, **attributes, &)
69
+ li_attributes, attributes = parse_li_attributes(attributes)
70
+
71
+ li(**li_attributes) do
72
+ details(open:) do
73
+ summary(**mix({ class: "menu-item is-clickable" }, attributes)) { label }
74
+ nested_list(&)
75
+ end
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ def parse_li_attributes(attributes)
82
+ if attributes.key?(:li_attributes)
83
+ [attributes[:li_attributes], attributes.except(:li_attributes)]
84
+ else
85
+ [{}, attributes]
86
+ end
87
+ end
88
+
89
+ def nested_list(&)
90
+ ul(class: "menu-list") do
91
+ yield(self)
92
+ end
93
+ end
94
+ end
95
+ end
@@ -8,9 +8,14 @@ module BulmaPhlex
8
8
  # `data_attributes_builder` option.
9
9
  class Modal < BulmaPhlex::Base
10
10
  StimulusDataAttributes = Data.define(:stimulus_controller) do
11
- def for_container = { controller: stimulus_controller }
12
- def for_background = { action: "click->bulma-phlex--modal#close" }
13
- def for_close_button = { action: "bulma-phlex--modal#close" }
11
+ def for_container
12
+ { controller: stimulus_controller,
13
+ action: ["keydown.esc@document->#{stimulus_controller}#close",
14
+ "command->#{stimulus_controller}#openOrCloseByCommand"] }
15
+ end
16
+
17
+ def for_background = { action: "click->#{stimulus_controller}#close" }
18
+ def for_close_button = { action: "#{stimulus_controller}#close" }
14
19
  end
15
20
 
16
21
  # **Parameters**
@@ -3,12 +3,16 @@
3
3
  module BulmaPhlex
4
4
  # Renders a dropdown menu for the [Bulma navbar](https://bulma.io/documentation/components/navbar/#dropdown-menu).
5
5
  #
6
- # Provides structured content for a navbar dropdown, including **headers**, **links**, and **dividers**.
7
- # Intended to be used inside a {BulmaPhlex::NavigationBar} block.
6
+ # Provides structured content for a navbar dropdown, including **headers**, **links**, and **dividers**. Pass boolean
7
+ # flags `right` or `boxed` to the constructor to customize the dropdown. This component is intended to be used inside
8
+ # a {BulmaPhlex::NavigationBar} block.
9
+ #
10
+ # The constructor and each of the three methods (`header`, `item`, and `divider`) can accept additional HTML
11
+ # attributes.
8
12
  #
9
13
  # ## Example
10
14
  #
11
- # render BulmaPhlex::NavigationBar.new do |navbar|
15
+ # render BulmaPhlex::NavigationBar.new(right: true) do |navbar|
12
16
  # navbar.brand_item "My App", "/"
13
17
  #
14
18
  # navbar.right do |menu|
@@ -17,28 +21,56 @@ module BulmaPhlex
17
21
  # dropdown.item "Profile", "/profile"
18
22
  # dropdown.item "Settings", "/settings"
19
23
  # dropdown.divider
20
- # dropdown.item "Sign Out", "/logout"
24
+ # dropdown.item "Sign Out", "/logout", data: { turbo_prefetch: "false" }
21
25
  # end
22
26
  # end
23
27
  # end
24
28
  class NavigationBarDropdown < BulmaPhlex::Base
29
+ # **Parameters**
30
+ #
31
+ # - `right` — If `true`, aligns the dropdown to the right side of the navbar
32
+ # - `boxed` — If `true`, applies the Bulma `is-boxed` style to the dropdown
33
+ # - `**html_attributes` — Additional HTML attributes for the dropdown container
34
+ def self.new(right: false, boxed: false, **html_attributes)
35
+ super
36
+ end
37
+
38
+ def initialize(right: false, boxed: false, **html_attributes)
39
+ @right = right
40
+ @boxed = boxed
41
+ @html_attributes = html_attributes
42
+ end
43
+
25
44
  def view_template(&)
26
- div(class: "navbar-dropdown is-right", &)
45
+ div(**mix({ class: navbar_dropdown_classes }, @html_attributes), &)
27
46
  end
28
47
 
29
48
  # Adds a non-clickable header item to the dropdown menu. Optionally add a divider before the header with
30
49
  # the `divder: true` parameter.
31
- def header(label, divider: false)
50
+ def header(label, divider: false, **html_attributes)
32
51
  self.divider if divider
33
- div(class: "navbar-item has-text-weight-semibold") { label }
52
+
53
+ attributes = mix({ class: "navbar-item has-text-weight-semibold" }, html_attributes)
54
+ div(**attributes) { label }
55
+ end
56
+
57
+ def item(label, path, **html_attributes)
58
+ attributes = mix({ class: "navbar-item", href: path }, html_attributes)
59
+ a(**attributes) { label }
34
60
  end
35
61
 
36
- def item(label, path)
37
- a(class: "navbar-item", href: path) { label }
62
+ def divider(**html_attributes)
63
+ attributes = mix({ class: "navbar-divider" }, html_attributes)
64
+ hr(**attributes)
38
65
  end
39
66
 
40
- def divider
41
- hr(class: "navbar-divider")
67
+ private
68
+
69
+ def navbar_dropdown_classes
70
+ classes = ["navbar-dropdown"]
71
+ classes << "is-right" if @right
72
+ classes << "is-boxed" if @boxed
73
+ classes.join(" ")
42
74
  end
43
75
  end
44
76
  end
@@ -11,7 +11,7 @@ module BulmaPhlex
11
11
  #
12
12
  # - `id` — Unique identifier for the tab
13
13
  # - `title` — The text displayed on the tab
14
- # - `icon` — Optional icon class to display on the tab (e.g. `"fas fa-cog"`)
14
+ # - `icon` — Optional icon class to display on the tab (e.g. `"fa-solid fa-gear"`)
15
15
  # - `active` — If `true`, marks the tab as currently active
16
16
  # - `data_attributes_proc` — A proc that generates data attributes for the tab
17
17
  def self.new(id:, title:, icon:, active:,
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BulmaPhlex
4
+ class Table
5
+ # Internal component for rendering a sortable table header.
6
+ class Sort < BulmaPhlex::Base
7
+ DIRECTIONS = %i[asc desc].freeze
8
+
9
+ # **Parameters**
10
+ #
11
+ # - `header_label` — The text to display in the table header
12
+ # - `header_classes` — CSS classes for the `<th>` element
13
+ # - `href` — The URL to navigate to when the header is clicked
14
+ # - `current_direction` — The current sort direction (`:asc`, `:desc`, or `nil`)
15
+ # - `link_attributes` — Additional HTML attributes for the `<a>` element
16
+ # - `icons` — Icon classes for `ascending`, `descending`, and `inactive` states
17
+ def self.new(header_label:, header_classes:, href:, current_direction: nil, link_attributes: {}, icons: nil)
18
+ super
19
+ end
20
+
21
+ def initialize(header_label:, header_classes:, href:, current_direction: nil, link_attributes: {}, icons: nil)
22
+ validate_direction!(current_direction)
23
+ validate_link_attributes!(link_attributes)
24
+ validate_icons!(icons) unless icons.nil?
25
+
26
+ @header_label = header_label
27
+ @header_classes = header_classes
28
+ @href = href
29
+ @direction = current_direction
30
+ @link_attributes = link_attributes
31
+ @icons = BulmaPhlex.config.icons.sort.merge(icons || {})
32
+ end
33
+
34
+ def view_template
35
+ th(**header_attributes) do
36
+ a(**link_attributes) do
37
+ render Icon.new(icon, text_left: @header_label, nowrap: true, icon_attributes: { aria: { hidden: "true" } })
38
+ end
39
+ end
40
+ end
41
+
42
+ def active?
43
+ !@direction.nil?
44
+ end
45
+
46
+ def ascending?
47
+ @direction == :asc
48
+ end
49
+
50
+ def descending?
51
+ @direction == :desc
52
+ end
53
+
54
+ attr_reader :href, :direction
55
+
56
+ private
57
+
58
+ def header_attributes
59
+ attributes = { class: @header_classes }
60
+ if active?
61
+ attributes[:aria_sort] = ascending? ? "ascending" : "descending"
62
+ end
63
+ attributes
64
+ end
65
+
66
+ def link_attributes
67
+ unless @link_attributes[:class]&.include?("has-text-")
68
+ @link_attributes = mix(@link_attributes,
69
+ class: "has-text-grey-dark")
70
+ end
71
+ mix(
72
+ @link_attributes,
73
+ href!: @href,
74
+ aria!: mix(@link_attributes.fetch(:aria, {}), label!: link_label)
75
+ )
76
+ end
77
+
78
+ def link_label
79
+ label = @header_label.to_s
80
+ return "Sort by #{label}" unless active?
81
+
82
+ sort_dir = ascending? ? "ascending" : "descending"
83
+ "#{label}, sorted #{sort_dir}. Activate to change sort order."
84
+ end
85
+
86
+ def icon
87
+ return @icons[:ascending] if ascending?
88
+ return @icons[:descending] if descending?
89
+
90
+ @icons[:inactive]
91
+ end
92
+
93
+ def validate_direction!(value)
94
+ return if value.nil? || DIRECTIONS.include?(value)
95
+
96
+ valid_directions = DIRECTIONS.map { |direction| ":#{direction}" }.join(", ")
97
+ raise ArgumentError, "current_direction must be #{valid_directions}, or nil"
98
+ end
99
+
100
+ def validate_link_attributes!(value)
101
+ raise ArgumentError, "link_attributes must be a Hash" unless value.is_a?(Hash)
102
+ end
103
+
104
+ def validate_icons!(value)
105
+ raise ArgumentError, "icons must be a Hash" unless value.is_a?(Hash)
106
+ end
107
+ end
108
+
109
+ private_constant :Sort
110
+ end
111
+ end
@@ -4,7 +4,8 @@ module BulmaPhlex
4
4
  # Renders the [Bulma table](https://bulma.io/documentation/elements/table/) component.
5
5
  #
6
6
  # Displays a collection of records in rows and columns. Columns are defined via the `column`,
7
- # `date_column`, and `conditional_icon` builder methods. Supports Bulma **style** options
7
+ # `date_column`, and `conditional_icon` builder methods. Headers can optionally be sortable by
8
+ # passing a sort hash to the column. Supports Bulma **style** options
8
9
  # (bordered, striped, hoverable) and **layout** options (narrow, fullwidth). An optional
9
10
  # **pagination** control can be added to the table footer via the `paginate` method.
10
11
  #
@@ -24,9 +25,11 @@ module BulmaPhlex
24
25
  #
25
26
  # render BulmaPhlex::Table.new(users) do |table|
26
27
  # table.row(class: "has-background-light") { |user| { id: "user-row-#{user.id}" } }
27
- # table.column("Name", &:full_name)
28
+ # table.column("Name", sort: { href: "/users?sort=name" }, &:full_name)
28
29
  # table.column("Email", hidden: "touch", &:email)
29
- # table.date_column("Joined", hidden: "mobile", &:created_at, format: "%B %d, %Y")
30
+ # table.date_column("Joined", hidden: "mobile", sort: {
31
+ # href: "/users?sort=created_at", current_direction: :descending
32
+ # }, &:created_at, format: "%B %d, %Y")
30
33
  # table.conditional_icon("Admin?", &:admin?)
31
34
  # table.column "Actions" do |user|
32
35
  # link_to "Edit", edit_user_path(user), class: "button is-small"
@@ -104,11 +107,15 @@ module BulmaPhlex
104
107
  # Adds a column to the table. Can be called multiple times to define all columns.
105
108
  #
106
109
  # - `header` — The column header text
110
+ # - `sort` — Optional hash containing `href`, `current_direction`, `link_attributes`,
111
+ # and `icons` for the generated link
107
112
  # - `**html_attributes` — Additional HTML attributes for each `<td>` cell in this column
108
113
  #
109
114
  # Expects a block that receives each `row` object and returns the cell content.
110
- def column(header, hidden: false, **html_attributes, &content)
111
- @columns << { header:, hidden:, html_attributes:, content: }
115
+ def column(header, hidden: false, sort: nil, **html_attributes, &content)
116
+ raise ArgumentError, "sort must be a Hash or nil" unless sort.nil? || sort.is_a?(Hash)
117
+
118
+ @columns << { header:, hidden:, html_attributes:, content:, sort: }
112
119
  end
113
120
 
114
121
  # Adds a date-formatted column to the table. Can be called multiple times.
@@ -118,8 +125,8 @@ module BulmaPhlex
118
125
  # - `**html_attributes` — Additional HTML attributes for each `<td>` cell in this column
119
126
  #
120
127
  # Expects a block that receives each `row` object and returns a `Date` or `Time` value.
121
- def date_column(header, hidden: false, format: "%Y-%m-%d", **html_attributes, &content)
122
- column(header, hidden:, **html_attributes) do |row|
128
+ def date_column(header, hidden: false, format: "%Y-%m-%d", sort: nil, **html_attributes, &content)
129
+ column(header, hidden:, sort:, **html_attributes) do |row|
123
130
  content.call(row)&.strftime(format)
124
131
  end
125
132
  end
@@ -127,14 +134,15 @@ module BulmaPhlex
127
134
  # Adds a column that displays an icon when the block returns a truthy value. Can be called multiple times.
128
135
  #
129
136
  # - `header` — The column header text
130
- # - `icon_class` — The CSS class(es) for the icon element (default: `"fas fa-check"`)
137
+ # - `icon_class` — The CSS class(es) for the icon element (default: configured conditional icon)
131
138
  # - `**html_attributes` — Additional HTML attributes for each `<td>` cell in this column
132
139
  #
133
140
  # Expects a block that receives each `row` object and returns a truthy or falsy value.
134
- def conditional_icon(header, hidden: false, icon_class: "fas fa-check", **html_attributes, &content)
141
+ def conditional_icon(header, hidden: false, icon_class: nil, sort: nil, **html_attributes, &content)
135
142
  html_attributes[:class] = [html_attributes[:class], "has-text-centered"].compact.join(" ")
143
+ icon_class ||= BulmaPhlex.config.icons.conditional
136
144
 
137
- column(header, hidden:, **html_attributes) do |row|
145
+ column(header, hidden:, sort:, **html_attributes) do |row|
138
146
  Icon(icon_class) if content.call(row)
139
147
  end
140
148
  end
@@ -150,7 +158,14 @@ module BulmaPhlex
150
158
  private
151
159
 
152
160
  def table_header(column)
153
- th(class: header_classes(column)) { column[:header] }
161
+ sort = column[:sort]
162
+ return th(class: header_classes(column)) { column[:header] } unless sort
163
+
164
+ render Sort.new(
165
+ header_label: column[:header],
166
+ header_classes: header_classes(column),
167
+ **sort
168
+ )
154
169
  end
155
170
 
156
171
  def header_classes(column)
@@ -18,11 +18,11 @@ module BulmaPhlex
18
18
  # "Profile content goes here"
19
19
  # end
20
20
  #
21
- # tabs.tab(id: "settings", title: "Settings", icon: "fas fa-cog") do
21
+ # tabs.tab(id: "settings", title: "Settings", icon: "fa-solid fa-gear") do
22
22
  # "Settings content goes here"
23
23
  # end
24
24
  #
25
- # tabs.tab(id: "notifications", title: "Notifications", icon: "fas fa-bell") do
25
+ # tabs.tab(id: "notifications", title: "Notifications", icon: "fa-solid fa-bell") do
26
26
  # "Notifications content goes here"
27
27
  # end
28
28
  # end
@@ -98,7 +98,7 @@ module BulmaPhlex
98
98
  #
99
99
  # - `id:` — A unique identifier for the tab, used to link the tab button to its content panel
100
100
  # - `title:` — The text label displayed on the tab button
101
- # - `icon:` — Optional icon class string (e.g. `"fas fa-cog"`) displayed alongside the title
101
+ # - `icon:` — Optional icon class string (e.g. `"fa-solid fa-gear"`) displayed alongside the title
102
102
  # - `active:` — If `true`, this tab is shown as selected on initial render (default: `false`)
103
103
  #
104
104
  # Expects a block that renders the content for this tab's panel.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BulmaPhlex
4
- VERSION = "0.17.0"
4
+ VERSION = "0.18.0"
5
5
  end
data/lib/bulma_phlex.rb CHANGED
@@ -14,4 +14,14 @@ loader.setup # ready!
14
14
  # Bulma-styled applications with a Ruby-focused components.
15
15
  module BulmaPhlex
16
16
  extend Phlex::Kit
17
+
18
+ class << self
19
+ def config
20
+ @config ||= Configuration.new
21
+ end
22
+
23
+ def configure
24
+ yield config
25
+ end
26
+ end
17
27
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bulma-phlex
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.0
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Todd Kummer
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-07-21 00:00:00.000000000 Z
10
+ date: 2026-09-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: phlex
@@ -69,6 +69,7 @@ files:
69
69
  - lib/bulma_phlex/button.rb
70
70
  - lib/bulma_phlex/card.rb
71
71
  - lib/bulma_phlex/columns.rb
72
+ - lib/bulma_phlex/configuration.rb
72
73
  - lib/bulma_phlex/dropdown.rb
73
74
  - lib/bulma_phlex/file_upload.rb
74
75
  - lib/bulma_phlex/form_control.rb
@@ -79,6 +80,7 @@ files:
79
80
  - lib/bulma_phlex/image.rb
80
81
  - lib/bulma_phlex/level.rb
81
82
  - lib/bulma_phlex/media_object.rb
83
+ - lib/bulma_phlex/menu.rb
82
84
  - lib/bulma_phlex/message.rb
83
85
  - lib/bulma_phlex/modal.rb
84
86
  - lib/bulma_phlex/navigation_bar.rb
@@ -89,6 +91,7 @@ files:
89
91
  - lib/bulma_phlex/tab_components/content.rb
90
92
  - lib/bulma_phlex/tab_components/tab.rb
91
93
  - lib/bulma_phlex/table.rb
94
+ - lib/bulma_phlex/table/sort.rb
92
95
  - lib/bulma_phlex/tabs.rb
93
96
  - lib/bulma_phlex/tag.rb
94
97
  - lib/bulma_phlex/title.rb