hakumi_components 0.1.16.pre → 0.1.17.pre

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 (134) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +169 -23
  3. data/app/assets/javascripts/hakumi_components.js +12 -12
  4. data/app/assets/stylesheets/hakumi_components.css +1 -1
  5. data/app/components/hakumi/alert/component.html.erb +12 -8
  6. data/app/components/hakumi/alert/component.rb +18 -62
  7. data/app/components/hakumi/base_component.rb +13 -0
  8. data/app/components/hakumi/card/component.html.erb +14 -22
  9. data/app/components/hakumi/card/component.rb +38 -31
  10. data/app/components/hakumi/checkbox/component.html.erb +39 -21
  11. data/app/components/hakumi/checkbox/component.rb +12 -2
  12. data/app/components/hakumi/collapse/component.html.erb +2 -2
  13. data/app/components/hakumi/collapse/component.rb +1 -1
  14. data/app/components/hakumi/collapse/panel/component.rb +9 -0
  15. data/app/components/hakumi/color_picker/component.rb +0 -4
  16. data/app/components/hakumi/drawer/component.html.erb +7 -7
  17. data/app/components/hakumi/drawer/component.rb +12 -19
  18. data/app/components/hakumi/input/component.rb +0 -2
  19. data/app/components/hakumi/input/text_area/component.rb +0 -2
  20. data/app/components/hakumi/input_number/component.rb +3 -4
  21. data/app/components/hakumi/mentions/component.rb +0 -1
  22. data/app/components/hakumi/modal/component.html.erb +40 -0
  23. data/app/components/hakumi/modal/component.rb +24 -102
  24. data/app/components/hakumi/modal/confirm/component.html.erb +23 -0
  25. data/app/components/hakumi/modal/confirm/component.rb +23 -41
  26. data/app/components/hakumi/modal/error/component.rb +12 -11
  27. data/app/components/hakumi/modal/info/component.rb +12 -11
  28. data/app/components/hakumi/modal/success/component.rb +12 -11
  29. data/app/components/hakumi/modal/warning/component.rb +15 -10
  30. data/app/components/hakumi/popconfirm/component.html.erb +25 -25
  31. data/app/components/hakumi/popconfirm/component.rb +11 -27
  32. data/app/components/hakumi/rate/component.rb +0 -1
  33. data/app/components/hakumi/segmented/component.rb +0 -4
  34. data/app/components/hakumi/slider/component.rb +2 -6
  35. data/app/components/hakumi/statistic/component.rb +0 -4
  36. data/app/components/hakumi/switch/component.html.erb +4 -0
  37. data/app/components/hakumi/switch/component.rb +1 -2
  38. data/app/components/hakumi/table/component.rb +3 -229
  39. data/app/components/hakumi/table/concerns/columns.rb +1 -1
  40. data/app/components/hakumi/table/concerns/editable.rb +121 -0
  41. data/app/components/hakumi/table/concerns/ellipsis.rb +63 -0
  42. data/app/components/hakumi/table/concerns/fixed_columns.rb +87 -0
  43. data/app/components/hakumi/transfer/component.rb +0 -4
  44. data/app/controllers/{hakumi_components → hakumi}/components_controller.rb +2 -2
  45. data/app/form_builders/hakumi/form_builder.rb +217 -175
  46. data/app/helpers/hakumi/form_helper.rb +39 -0
  47. data/app/javascript/hakumi_components/controllers/base/registry_controller.js +83 -3
  48. data/app/javascript/hakumi_components/controllers/hakumi/affix_controller.js +0 -23
  49. data/app/javascript/hakumi_components/controllers/hakumi/alert_controller.js +2 -1
  50. data/app/javascript/hakumi_components/controllers/hakumi/button_controller.js +0 -7
  51. data/app/javascript/hakumi_components/controllers/hakumi/calendar_controller.js +0 -2
  52. data/app/javascript/hakumi_components/controllers/hakumi/color_picker_controller.js +1 -6
  53. data/app/javascript/hakumi_components/controllers/hakumi/date_picker_controller.js +28 -34
  54. data/app/javascript/hakumi_components/controllers/hakumi/drawer_controller.js +2 -1
  55. data/app/javascript/hakumi_components/controllers/hakumi/form_item_controller.js +9 -63
  56. data/app/javascript/hakumi_components/controllers/hakumi/mentions_controller.js +4 -11
  57. data/app/javascript/hakumi_components/controllers/hakumi/message_controller.js +1 -1
  58. data/app/javascript/hakumi_components/controllers/hakumi/modal_controller.js +4 -20
  59. data/app/javascript/hakumi_components/controllers/hakumi/notification_controller.js +1 -1
  60. data/app/javascript/hakumi_components/controllers/hakumi/popconfirm_controller.js +33 -27
  61. data/app/javascript/hakumi_components/controllers/hakumi/popover_controller.js +2 -23
  62. data/app/javascript/hakumi_components/controllers/hakumi/qr_code_controller.js +0 -20
  63. data/app/javascript/hakumi_components/controllers/hakumi/segmented_controller.js +0 -2
  64. data/app/javascript/hakumi_components/controllers/hakumi/spin_controller.js +1 -19
  65. data/app/javascript/hakumi_components/controllers/hakumi/statistic_controller.js +0 -2
  66. data/app/javascript/hakumi_components/controllers/hakumi/table_controller.js +48 -74
  67. data/app/javascript/hakumi_components/controllers/hakumi/tag_controller.js +15 -14
  68. data/app/javascript/hakumi_components/controllers/hakumi/tag_group_controller.js +14 -13
  69. data/app/javascript/hakumi_components/controllers/hakumi/theme_controller.js +24 -1
  70. data/app/javascript/hakumi_components/controllers/hakumi/time_picker_controller.js +3 -7
  71. data/app/javascript/hakumi_components/controllers/hakumi/timeline_controller.js +0 -16
  72. data/app/javascript/hakumi_components/controllers/hakumi/transfer_controller.js +2 -2
  73. data/app/javascript/hakumi_components/controllers/hakumi/tree_controller.js +0 -2
  74. data/app/javascript/hakumi_components/controllers/hakumi/tree_select_controller.js +3 -3
  75. data/app/javascript/hakumi_components/controllers/hakumi/upload_controller.js +12 -26
  76. data/app/javascript/hakumi_components/core/persistence.js +3 -3
  77. data/app/javascript/hakumi_components/core/render_component.js +3 -1
  78. data/app/javascript/lib/validation_manager.js +101 -0
  79. data/app/javascript/stylesheets/_theme-tokens.scss +2 -1
  80. data/app/javascript/stylesheets/components/_modal.scss +13 -0
  81. data/app/services/{hakumi_components → hakumi}/component_handler.rb +1 -1
  82. data/app/services/hakumi/icon/loader.rb +2 -2
  83. data/app/services/hakumi/illustrations/loader.rb +3 -3
  84. data/app/views/hakumi/_drawer.html.erb +21 -0
  85. data/app/views/hakumi/_modal.html.erb +18 -0
  86. data/lib/hakumi_components/documentation.rb +127 -0
  87. data/lib/hakumi_components/engine.rb +13 -4
  88. data/lib/hakumi_components/rails/attribute_introspection.rb +1 -1
  89. data/lib/hakumi_components/rails/validation_introspection.rb +5 -5
  90. data/lib/hakumi_components/rails/validation_mapper.rb +484 -0
  91. data/lib/hakumi_components/rails.rb +2 -1
  92. data/lib/hakumi_components/version.rb +2 -2
  93. data/lib/hakumi_components.rb +3 -1
  94. data/lib/tasks/coverage.rake +37 -0
  95. data/sig/hakumi/base_component.rbs +5 -0
  96. data/sig/hakumi/checkbox/component.rbs +10 -0
  97. data/sig/hakumi/color_picker/component.rbs +0 -1
  98. data/sig/hakumi/form_builder.rbs +9 -1
  99. data/sig/{hakumi_components → hakumi}/rails/attribute_introspection.rbs +1 -1
  100. data/sig/{hakumi_components → hakumi}/rails/validation_introspection.rbs +1 -1
  101. data/sig/hakumi/rails/validation_mapper.rbs +53 -0
  102. data/sig/{hakumi_components → hakumi}/rails.rbs +1 -1
  103. data/sig/hakumi/segmented/component.rbs +0 -1
  104. data/sig/hakumi/slider/component.rbs +0 -1
  105. data/sig/hakumi/statistic/component.rbs +0 -2
  106. data/sig/hakumi/table/component.rbs +3 -4
  107. data/sig/hakumi/table/concerns/columns.rbs +2 -1
  108. data/sig/hakumi/table/concerns/editable.rbs +40 -0
  109. data/sig/hakumi/table/concerns/ellipsis.rbs +27 -0
  110. data/sig/hakumi/table/concerns/fixed_columns.rbs +33 -0
  111. data/sig/hakumi/transfer/component.rbs +0 -1
  112. data/sig/{hakumi_components.rbs → hakumi.rbs} +20 -3
  113. data/sig/rails/active_model/validations/comparison_validator.rbs +6 -0
  114. metadata +44 -29
  115. data/app/views/hakumi_components/_drawer.html.erb +0 -3
  116. data/app/views/hakumi_components/_modal.html.erb +0 -3
  117. /data/app/views/{hakumi_components → hakumi}/_admin_panel.html.erb +0 -0
  118. /data/app/views/{hakumi_components → hakumi}/_affix.html.erb +0 -0
  119. /data/app/views/{hakumi_components → hakumi}/_alert.html.erb +0 -0
  120. /data/app/views/{hakumi_components → hakumi}/_confirm.html.erb +0 -0
  121. /data/app/views/{hakumi_components → hakumi}/_message.html.erb +0 -0
  122. /data/app/views/{hakumi_components → hakumi}/_notification.html.erb +0 -0
  123. /data/app/views/{hakumi_components → hakumi}/_popconfirm.html.erb +0 -0
  124. /data/app/views/{hakumi_components → hakumi}/_popover.html.erb +0 -0
  125. /data/app/views/{hakumi_components → hakumi}/_qr_code.html.erb +0 -0
  126. /data/app/views/{hakumi_components → hakumi}/_result.html.erb +0 -0
  127. /data/app/views/{hakumi_components → hakumi}/_segmented.html.erb +0 -0
  128. /data/app/views/{hakumi_components → hakumi}/_skeleton.html.erb +0 -0
  129. /data/app/views/{hakumi_components → hakumi}/_spin.html.erb +0 -0
  130. /data/app/views/{hakumi_components → hakumi}/_statistic.html.erb +0 -0
  131. /data/app/views/{hakumi_components → hakumi}/_table.html.erb +0 -0
  132. /data/app/views/{hakumi_components → hakumi}/_tag.html.erb +0 -0
  133. /data/app/views/{hakumi_components → hakumi}/_timeline.html.erb +0 -0
  134. /data/app/views/{hakumi_components → hakumi}/_tree.html.erb +0 -0
@@ -255,10 +255,6 @@ module Hakumi
255
255
  .gsub("ss", seconds.to_s.rjust(2, "0"))
256
256
  end
257
257
 
258
- def cast_boolean(value)
259
- ActiveModel::Type::Boolean.new.cast(value)
260
- end
261
-
262
258
  def normalize_precision(value)
263
259
  return nil if value.nil?
264
260
  return value if value.is_a?(Integer)
@@ -1,4 +1,8 @@
1
1
  <% switch_control = capture do %>
2
+ <% # Rails convention: hidden field with value="0" ensures unchecked state sends params %>
3
+ <% if @name && !standalone? %>
4
+ <%= tag.input(type: "hidden", name: @name, value: "0", autocomplete: "off") %>
5
+ <% end %>
2
6
  <%= content_tag(:label, **wrapper_attributes) do %>
3
7
  <%= tag.input(**input_attributes) %>
4
8
  <span class="hakumi-switch-handle">
@@ -23,7 +23,7 @@ module Hakumi
23
23
  errors: [],
24
24
  **html_options
25
25
  )
26
- @name = name || "switch_#{SecureRandom.hex(4)}"
26
+ @name = name || generate_id("switch")
27
27
  @label = label
28
28
  @caption = caption
29
29
  @checked = checked.nil? ? default_checked : checked
@@ -96,7 +96,6 @@ module Hakumi
96
96
  class: [ "hakumi-switch-input", @html_options[:class] ].compact.join(" "),
97
97
  checked: @checked ? true : nil,
98
98
  disabled: (@disabled || @loading) ? true : nil,
99
- required: @required ? true : nil,
100
99
  "aria-invalid": has_error? ? "true" : nil,
101
100
  "aria-describedby": describedby_ids,
102
101
  tabindex: "-1",
@@ -5,6 +5,9 @@ module Hakumi
5
5
  # A complex Table component supporting sorting, filtering, selection, expansion, and editing.
6
6
  class Component < Hakumi::BaseComponent
7
7
  include Concerns::Columns
8
+ include Concerns::Editable
9
+ include Concerns::FixedColumns
10
+ include Concerns::Ellipsis
8
11
 
9
12
  # @group Configuration Constants
10
13
  SIZES = %i[default middle small].freeze
@@ -12,9 +15,6 @@ module Hakumi
12
15
  SORT_DIRECTIONS = %i[ascend descend].freeze
13
16
  FIXED_SIDES = %i[left right].freeze
14
17
  RESPONSIVE_BREAKPOINTS = %i[xs sm md lg xl xxl].freeze
15
- EDITABLE_MODES = %i[cell row].freeze
16
- EDITABLE_TRIGGERS = %i[click dblclick].freeze
17
- EDITABLE_INPUT_TYPES = %i[text number textarea select].freeze
18
18
  ROW_BUTTON_ACTIONS = %i[edit save cancel].freeze
19
19
 
20
20
  # @group Slot Definitions
@@ -538,10 +538,6 @@ module Hakumi
538
538
  }
539
539
  end
540
540
 
541
- def fixed_columns?
542
- fixed_offsets.present?
543
- end
544
-
545
541
  private
546
542
 
547
543
  def normalize_row_selection!
@@ -575,132 +571,6 @@ module Hakumi
575
571
  }.compact.presence
576
572
  end
577
573
 
578
- def cast_boolean(value)
579
- ActiveModel::Type::Boolean.new.cast(value)
580
- end
581
-
582
- def fixed_offsets
583
- @fixed_offsets ||= {}
584
- end
585
-
586
- def apply_fixed_column_attributes!(attrs, column, header: false)
587
- info = fixed_offsets[column[:key]]
588
- return unless info
589
-
590
- attrs[:class] = [ attrs[:class], "hakumi-table-cell-fixed", "hakumi-table-cell-fixed-#{info[:side]}" ].compact.join(" ")
591
-
592
- style_parts = []
593
- style_parts << attrs[:style] if attrs[:style].present?
594
- style_parts << "position: sticky"
595
- style_parts << "#{info[:side]}: #{info[:offset]}px"
596
- style_parts << "z-index: #{header ? 3 : 2}"
597
- attrs[:style] = style_parts.compact.join("; ")
598
-
599
- attrs[:data] ||= {}
600
- attrs[:data][:fixed] = info[:side]
601
- end
602
-
603
- def apply_editable_attributes!(attrs, column)
604
- payload = cell_editable_payload(column)
605
- return unless payload
606
-
607
- attrs[:class] = [ attrs[:class], "hakumi-table-cell-editable" ].compact.join(" ")
608
- attrs[:data] ||= {}
609
- attrs[:data][:editable] = true
610
- attrs[:data][:editableConfig] = payload.to_json
611
- attrs[:data][:action] = "click->hakumi--table#editCell"
612
- end
613
-
614
- def compute_fixed_offsets(leaf_columns)
615
- @fixed_offsets = {}
616
- return if leaf_columns.blank?
617
-
618
- left_offset = 0
619
- leaf_columns.each do |column|
620
- next unless column[:fixed] == :left
621
-
622
- width = fixed_column_width!(column)
623
- @fixed_offsets[column[:key]] = { side: :left, offset: left_offset }
624
- left_offset += width
625
- end
626
-
627
- right_offset = 0
628
- leaf_columns.reverse_each do |column|
629
- next unless column[:fixed] == :right
630
-
631
- width = fixed_column_width!(column)
632
- @fixed_offsets[column[:key]] = { side: :right, offset: right_offset }
633
- right_offset += width
634
- end
635
- end
636
-
637
- def fixed_column_width!(column)
638
- width = numeric_width(column[:width])
639
- return width if width&.positive?
640
-
641
- raise ArgumentError, "Column #{column[:key] || column[:data_index] || column[:title]} must define a pixel width when fixed"
642
- end
643
-
644
- def numeric_width(value)
645
- return value.to_f if value.is_a?(Numeric)
646
- return nil unless value.is_a?(String)
647
-
648
- stripped = value.strip
649
- return stripped.to_f if stripped.match?(/\A[\d.]+\z/)
650
-
651
- match = stripped.match(/\A([\d.]+)px\z/i)
652
- match ? match[1].to_f : nil
653
- end
654
-
655
- def ellipsis_config(column)
656
- column[:ellipsis]
657
- end
658
-
659
- def normalize_ellipsis_option(option)
660
- return nil if option.nil? || option == false
661
-
662
- if option == true
663
- { show_title: true }
664
- elsif option.is_a?(Hash)
665
- normalized = option.deep_symbolize_keys
666
- normalized[:tooltip] = normalize_ellipsis_tooltip_option(normalized[:tooltip])
667
- normalized.compact.presence
668
- else
669
- { tooltip: normalize_ellipsis_tooltip_option(option) }
670
- end
671
- end
672
-
673
- def normalize_ellipsis_tooltip_option(option)
674
- return nil if option.nil? || option == false
675
- return {} if option == true
676
- return { title: option } if option.is_a?(String)
677
- return option.deep_symbolize_keys if option.is_a?(Hash)
678
-
679
- nil
680
- end
681
-
682
- def ellipsis_title(config, raw_value, content_fragment)
683
- return nil unless config&.dig(:show_title)
684
-
685
- value = raw_value.presence || content_fragment
686
- value = value.to_s if value.respond_to?(:to_s)
687
- value.present? ? value : nil
688
- end
689
-
690
- def ellipsis_tooltip_component(config, raw_value, row, row_index)
691
- tooltip = config&.dig(:tooltip)
692
- return nil unless tooltip
693
-
694
- options = tooltip.deep_dup
695
- title = options.delete(:title)
696
- title ||= raw_value
697
- title = title.call(row, row_index) if title.respond_to?(:call)
698
- title = title.to_s if title.respond_to?(:to_s)
699
- return nil if title.blank?
700
-
701
- Hakumi::Tooltip::Component.new(**{ title: title }.merge(options))
702
- end
703
-
704
574
  def responsive_classes(column)
705
575
  responsive = column[:responsive]
706
576
  return [] unless responsive.present?
@@ -823,33 +693,6 @@ module Hakumi
823
693
  }
824
694
  end
825
695
 
826
- # Normalizes the table-level editable configuration.
827
- # Accepts boolean, symbol (:cell or :row), or hash with options.
828
- # Returns nil if editing is disabled, or a hash with normalized options.
829
- #
830
- # @param value [Boolean, Symbol, Hash, nil] editable configuration
831
- # @return [Hash, nil] normalized configuration
832
- def normalize_editable(value)
833
- return nil if value.nil? || value == false
834
-
835
- case value
836
- when true
837
- { mode: :cell, trigger: :click }
838
- when Symbol
839
- mode = EDITABLE_MODES.include?(value) ? value : :cell
840
- { mode: mode, trigger: :click }
841
- when Hash
842
- normalized = value.deep_symbolize_keys
843
- mode = normalized[:mode]&.to_sym
844
- normalized[:mode] = EDITABLE_MODES.include?(mode) ? mode : :cell
845
- trigger = normalized[:trigger]&.to_sym
846
- normalized[:trigger] = EDITABLE_TRIGGERS.include?(trigger) ? trigger : :click
847
- normalized
848
- else
849
- nil
850
- end
851
- end
852
-
853
696
  # Normalizes row drag configuration.
854
697
  # Can be: true (drag whole row), :handle (drag via handle only), or Hash with options
855
698
  #
@@ -874,44 +717,6 @@ module Hakumi
874
717
  end
875
718
  end
876
719
 
877
- # Normalizes column-level editable configuration.
878
- # Column editable can override table-level settings.
879
- #
880
- # @param value [Boolean, Hash, nil] column editable configuration
881
- # @return [Hash, nil] normalized configuration
882
- def normalize_column_editable(value)
883
- return nil if value.nil? || value == false
884
- return {} if value == true
885
-
886
- return nil unless value.is_a?(Hash)
887
-
888
- normalized = value.deep_symbolize_keys
889
- input_type = normalized[:type]&.to_sym || normalized[:input_type]&.to_sym
890
- normalized[:input_type] = EDITABLE_INPUT_TYPES.include?(input_type) ? input_type : :text
891
- normalized.except(:type)
892
- end
893
-
894
- def normalize_should_cell_update(value)
895
- case value
896
- when Symbol
897
- value.to_s
898
- when String
899
- value.strip.presence
900
- else
901
- nil
902
- end
903
- end
904
-
905
- # Returns JSON configuration for the table-level editable settings
906
- # to be passed to the Stimulus controller.
907
- #
908
- # @return [String, nil] JSON string or nil if not editable
909
- def editable_config_json
910
- return nil unless @editable
911
-
912
- @editable.to_json
913
- end
914
-
915
720
  def columns_config_json
916
721
  leaf_columns.map do |col|
917
722
  {
@@ -921,37 +726,6 @@ module Hakumi
921
726
  }.compact
922
727
  end.to_json
923
728
  end
924
-
925
- # Returns the editable payload for a specific cell/column.
926
- # Used to add data-editable-config attribute to editable cells.
927
- #
928
- # @param column [Hash] the column definition
929
- # @return [Hash, nil] editable configuration for the cell
930
- def cell_editable_payload(column)
931
- return nil if column[:selection] || column[:expand]
932
- return nil unless column[:data_index]
933
-
934
- column_config = column[:editable]
935
-
936
- # Column must explicitly opt-in to be editable
937
- return nil if column_config.nil? || column_config == false
938
-
939
- # Merge table-level and column-level config
940
- base_config = @editable&.dup || {}
941
- base_config.merge!(column_config) if column_config.is_a?(Hash)
942
-
943
- base_config[:data_index] = column[:data_index]
944
- base_config[:key] = column[:key]
945
- base_config
946
- end
947
-
948
- def apply_should_cell_update_attributes!(attrs, column)
949
- hook = column[:should_cell_update]
950
- return unless hook.present?
951
-
952
- attrs[:data] ||= {}
953
- attrs[:data][:should_cell_update] = hook
954
- end
955
729
  end
956
730
  end
957
731
  end
@@ -66,7 +66,7 @@ module Hakumi
66
66
  normalized = column.deep_symbolize_keys
67
67
  return nil if normalized[:hidden]
68
68
 
69
- normalized[:key] ||= normalized[:data_index] || SecureRandom.hex(4)
69
+ normalized[:key] ||= normalized[:data_index] || generate_id("col")
70
70
  normalized[:ellipsis] = normalize_ellipsis_option(normalized[:ellipsis])
71
71
  normalized[:responsive] = normalize_responsive_breakpoints(normalized[:responsive])
72
72
  normalized[:width] = dimension_to_css(normalized[:width])
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hakumi
4
+ module Table
5
+ module Concerns
6
+ # Editable cell and row functionality for Table component
7
+ module Editable
8
+ extend ActiveSupport::Concern
9
+
10
+ EDITABLE_MODES = %i[cell row].freeze
11
+ EDITABLE_TRIGGERS = %i[click dblclick].freeze
12
+ EDITABLE_INPUT_TYPES = %i[text number textarea select].freeze
13
+
14
+ # Returns JSON configuration for the table-level editable settings
15
+ # to be passed to the Stimulus controller.
16
+ #
17
+ # @return [String, nil] JSON string or nil if not editable
18
+ def editable_config_json
19
+ return nil unless @editable
20
+
21
+ @editable.to_json
22
+ end
23
+
24
+ private
25
+
26
+ # Normalizes the table-level editable configuration.
27
+ # Accepts boolean, symbol (:cell or :row), or hash with options.
28
+ # Returns nil if editing is disabled, or a hash with normalized options.
29
+ #
30
+ # @param value [Boolean, Symbol, Hash, nil] editable configuration
31
+ # @return [Hash, nil] normalized configuration
32
+ def normalize_editable(value)
33
+ return nil if value.nil? || value == false
34
+
35
+ case value
36
+ when true
37
+ { mode: :cell, trigger: :click }
38
+ when Symbol
39
+ mode = EDITABLE_MODES.include?(value) ? value : :cell
40
+ { mode: mode, trigger: :click }
41
+ when Hash
42
+ normalized = value.deep_symbolize_keys
43
+ mode = normalized[:mode]&.to_sym
44
+ normalized[:mode] = EDITABLE_MODES.include?(mode) ? mode : :cell
45
+ trigger = normalized[:trigger]&.to_sym
46
+ normalized[:trigger] = EDITABLE_TRIGGERS.include?(trigger) ? trigger : :click
47
+ normalized
48
+ end
49
+ end
50
+
51
+ # Normalizes column-level editable configuration.
52
+ # Column editable can override table-level settings.
53
+ #
54
+ # @param value [Boolean, Hash, nil] column editable configuration
55
+ # @return [Hash, nil] normalized configuration
56
+ def normalize_column_editable(value)
57
+ return nil if value.nil? || value == false
58
+ return {} if value == true
59
+ return nil unless value.is_a?(Hash)
60
+
61
+ normalized = value.deep_symbolize_keys
62
+ input_type = normalized[:type]&.to_sym || normalized[:input_type]&.to_sym
63
+ normalized[:input_type] = EDITABLE_INPUT_TYPES.include?(input_type) ? input_type : :text
64
+ normalized.except(:type)
65
+ end
66
+
67
+ def normalize_should_cell_update(value)
68
+ case value
69
+ when Symbol
70
+ value.to_s
71
+ when String
72
+ value.strip.presence
73
+ end
74
+ end
75
+
76
+ # Applies editable attributes to cell
77
+ def apply_editable_attributes!(attrs, column)
78
+ payload = cell_editable_payload(column)
79
+ return unless payload
80
+
81
+ attrs[:class] = [ attrs[:class], "hakumi-table-cell-editable" ].compact.join(" ")
82
+ attrs[:data] ||= {}
83
+ attrs[:data][:editable] = true
84
+ attrs[:data][:editableConfig] = payload.to_json
85
+ attrs[:data][:action] = "click->hakumi--table#editCell"
86
+ end
87
+
88
+ def apply_should_cell_update_attributes!(attrs, column)
89
+ hook = column[:should_cell_update]
90
+ return unless hook.present?
91
+
92
+ attrs[:data] ||= {}
93
+ attrs[:data][:should_cell_update] = hook
94
+ end
95
+
96
+ # Returns the editable payload for a specific cell/column.
97
+ # Used to add data-editable-config attribute to editable cells.
98
+ #
99
+ # @param column [Hash] the column definition
100
+ # @return [Hash, nil] editable configuration for the cell
101
+ def cell_editable_payload(column)
102
+ return nil if column[:selection] || column[:expand]
103
+ return nil unless column[:data_index]
104
+
105
+ column_config = column[:editable]
106
+
107
+ # Column must explicitly opt-in to be editable
108
+ return nil if column_config.nil? || column_config == false
109
+
110
+ # Merge table-level and column-level config
111
+ base_config = @editable&.dup || {}
112
+ base_config.merge!(column_config) if column_config.is_a?(Hash)
113
+
114
+ base_config[:data_index] = column[:data_index]
115
+ base_config[:key] = column[:key]
116
+ base_config
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hakumi
4
+ module Table
5
+ module Concerns
6
+ # Text ellipsis and tooltip handling for Table cells
7
+ module Ellipsis
8
+ extend ActiveSupport::Concern
9
+
10
+ private
11
+
12
+ def ellipsis_config(column)
13
+ column[:ellipsis]
14
+ end
15
+
16
+ def normalize_ellipsis_option(option)
17
+ return nil if option.nil? || option == false
18
+
19
+ if option == true
20
+ { show_title: true }
21
+ elsif option.is_a?(Hash)
22
+ normalized = option.deep_symbolize_keys
23
+ normalized[:tooltip] = normalize_ellipsis_tooltip_option(normalized[:tooltip])
24
+ normalized.compact.presence
25
+ else
26
+ { tooltip: normalize_ellipsis_tooltip_option(option) }
27
+ end
28
+ end
29
+
30
+ def normalize_ellipsis_tooltip_option(option)
31
+ return nil if option.nil? || option == false
32
+ return {} if option == true
33
+ return { title: option } if option.is_a?(String)
34
+ return option.deep_symbolize_keys if option.is_a?(Hash)
35
+
36
+ nil
37
+ end
38
+
39
+ def ellipsis_title(config, raw_value, content_fragment)
40
+ return nil unless config&.dig(:show_title)
41
+
42
+ value = raw_value.presence || content_fragment
43
+ value = value.to_s if value.respond_to?(:to_s)
44
+ value.present? ? value : nil
45
+ end
46
+
47
+ def ellipsis_tooltip_component(config, raw_value, row, row_index)
48
+ tooltip = config&.dig(:tooltip)
49
+ return nil unless tooltip
50
+
51
+ options = tooltip.deep_dup
52
+ title = options.delete(:title)
53
+ title ||= raw_value
54
+ title = title.call(row, row_index) if title.respond_to?(:call)
55
+ title = title.to_s if title.respond_to?(:to_s)
56
+ return nil if title.blank?
57
+
58
+ Hakumi::Tooltip::Component.new(**{ title: title }.merge(options))
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hakumi
4
+ module Table
5
+ module Concerns
6
+ # Fixed (sticky) column positioning for Table component
7
+ module FixedColumns
8
+ extend ActiveSupport::Concern
9
+
10
+ # Check if table has any fixed columns
11
+ def fixed_columns?
12
+ fixed_offsets.present?
13
+ end
14
+
15
+ private
16
+
17
+ def fixed_offsets
18
+ @fixed_offsets ||= {}
19
+ end
20
+
21
+ # Applies fixed column positioning attributes to cell
22
+ def apply_fixed_column_attributes!(attrs, column, header: false)
23
+ info = fixed_offsets[column[:key]]
24
+ return unless info
25
+
26
+ attrs[:class] = [ attrs[:class], "hakumi-table-cell-fixed", "hakumi-table-cell-fixed-#{info[:side]}" ].compact.join(" ")
27
+
28
+ style_parts = Array.new
29
+ style_parts << attrs[:style] if attrs[:style].present?
30
+ style_parts << "position: sticky"
31
+ style_parts << "#{info[:side]}: #{info[:offset]}px"
32
+ style_parts << "z-index: #{header ? 3 : 2}"
33
+ attrs[:style] = style_parts.compact.join("; ")
34
+
35
+ attrs[:data] ||= {}
36
+ attrs[:data][:fixed] = info[:side]
37
+ end
38
+
39
+ # Compute fixed column offsets from left and right
40
+ def compute_fixed_offsets(leaf_columns)
41
+ @fixed_offsets = {}
42
+ return if leaf_columns.blank?
43
+
44
+ # Calculate left fixed columns
45
+ left_offset = 0
46
+ leaf_columns.each do |column|
47
+ next unless column[:fixed] == :left
48
+
49
+ width = fixed_column_width!(column)
50
+ @fixed_offsets[column[:key]] = { side: :left, offset: left_offset }
51
+ left_offset += width
52
+ end
53
+
54
+ # Calculate right fixed columns (from right to left)
55
+ right_offset = 0
56
+ leaf_columns.reverse_each do |column|
57
+ next unless column[:fixed] == :right
58
+
59
+ width = fixed_column_width!(column)
60
+ @fixed_offsets[column[:key]] = { side: :right, offset: right_offset }
61
+ right_offset += width
62
+ end
63
+ end
64
+
65
+ def fixed_column_width!(column)
66
+ width = numeric_width(column[:width])
67
+ return width if width&.positive?
68
+
69
+ raise ArgumentError, "Column #{column[:key] || column[:data_index] || column[:title]} must define a pixel width when fixed"
70
+ end
71
+
72
+ def numeric_width(value)
73
+ case value
74
+ when Integer, Float
75
+ value.to_f
76
+ when String
77
+ stripped = value.strip
78
+ return stripped.to_f if stripped.match?(/\A[\d.]+\z/)
79
+
80
+ match = stripped.match(/\A([\d.]+)px\z/i)
81
+ match ? match[1].to_f : nil
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -196,10 +196,6 @@ module Hakumi
196
196
  }
197
197
  end
198
198
 
199
- def cast_boolean(value)
200
- ActiveModel::Type::Boolean.new.cast(value)
201
- end
202
-
203
199
  def normalize_string(value)
204
200
  return nil if value.nil?
205
201
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module HakumiComponents
3
+ module Hakumi
4
4
  class ComponentsController < ApplicationController
5
5
  COMPONENTS = %i[modal confirm alert drawer message admin_panel notification popconfirm popover result skeleton spin affix qr_code segmented statistic table tag timeline tree].freeze
6
6
 
@@ -16,7 +16,7 @@ module HakumiComponents
16
16
  raise ActionController::RoutingError, "Not Found" unless COMPONENTS.include?(component)
17
17
 
18
18
  handler = ComponentHandler.new(component, params)
19
- html = render_to_string(partial: "hakumi_components/#{component}", formats: [ :html ], locals: handler.locals)
19
+ html = render_to_string(partial: "hakumi/#{component}", formats: [ :html ], locals: handler.locals)
20
20
 
21
21
  respond_to do |format|
22
22
  format.html { render html: html, layout: false }