odt-ui-rails 2.2.0 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +308 -44
- data/app/assets/stylesheets/odt_ui/styles.css +420 -0
- data/app/javascript/controllers/odt_modal_controller.js +61 -0
- data/app/javascript/controllers/odt_toast_controller.js +40 -0
- data/lib/generators/odt/install_generator.rb +17 -0
- data/lib/generators/odt/templates/odt_modal_controller.js +61 -0
- data/lib/generators/odt/templates/odt_toast_controller.js +40 -0
- data/lib/odt_ui/helper.rb +265 -0
- data/lib/odt_ui/version.rb +1 -1
- data/sig/odt_ui.rbs +72 -0
- metadata +170 -4
data/lib/odt_ui/helper.rb
CHANGED
|
@@ -677,6 +677,271 @@ module OdtUi
|
|
|
677
677
|
end
|
|
678
678
|
end
|
|
679
679
|
|
|
680
|
+
# Renders an ODT Toaster viewport container.
|
|
681
|
+
#
|
|
682
|
+
# @param position [:top_right, :top_center, :top_left, :bottom_right, :bottom_center, :bottom_left]
|
|
683
|
+
# @param id [String] Element ID for Turbo Stream targeting (default: "odt-toasts")
|
|
684
|
+
# @return [ActiveSupport::SafeBuffer]
|
|
685
|
+
def odt_toaster(position: :top_right, id: "odt-toasts", **html_options, &block)
|
|
686
|
+
pos_key = position.to_s.tr("_", "-")
|
|
687
|
+
classes = [
|
|
688
|
+
"odt-toaster",
|
|
689
|
+
"odt-toaster--#{pos_key}",
|
|
690
|
+
html_options.delete(:class)
|
|
691
|
+
]
|
|
692
|
+
|
|
693
|
+
opts = html_options.merge(
|
|
694
|
+
id: id,
|
|
695
|
+
class: classes.compact.reject(&:blank?).join(" "),
|
|
696
|
+
role: "region",
|
|
697
|
+
aria: { live: "polite" }
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
inner_content = block_given? ? capture(&block) : nil
|
|
701
|
+
tag.div(inner_content, **opts)
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
# Renders a single ODT Toast card with Stimulus controller.
|
|
705
|
+
#
|
|
706
|
+
# @param message [String, nil]
|
|
707
|
+
# @param title [String, nil]
|
|
708
|
+
# @param variant [:success, :warning, :danger, :error, :info, :loading, :frosted]
|
|
709
|
+
# @param icon [String, nil]
|
|
710
|
+
# @param duration [Integer] Auto-dismiss duration in milliseconds (0 to disable)
|
|
711
|
+
# @param dismissible [Boolean] Whether to display close button
|
|
712
|
+
# @param radius [:none, :xs, :sm, :md, :lg, :xl, :"2xl", :"3xl", :"4xl", :full, nil]
|
|
713
|
+
# @return [ActiveSupport::SafeBuffer]
|
|
714
|
+
def odt_toast(message = nil, title: nil, variant: :success, icon: nil, duration: 4000, dismissible: true, radius: nil, **html_options, &block)
|
|
715
|
+
variant_key = variant.to_sym == :error ? :danger : variant.to_sym
|
|
716
|
+
default_icons = {
|
|
717
|
+
success: "fa-solid fa-circle-check",
|
|
718
|
+
warning: "fa-solid fa-triangle-exclamation",
|
|
719
|
+
danger: "fa-solid fa-circle-xmark",
|
|
720
|
+
info: "fa-solid fa-circle-info",
|
|
721
|
+
loading: "fa-solid fa-spinner"
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
icon_name = icon || default_icons[variant_key] || default_icons[:info]
|
|
725
|
+
|
|
726
|
+
classes = [
|
|
727
|
+
"odt-toast",
|
|
728
|
+
"odt-toast--#{variant_key.to_s.tr('_', '-')}"
|
|
729
|
+
]
|
|
730
|
+
classes << "odt-radius-#{radius.to_s.tr('_', '-')}" if radius.present?
|
|
731
|
+
classes << html_options.delete(:class)
|
|
732
|
+
|
|
733
|
+
data_attrs = {
|
|
734
|
+
controller: "odt-toast",
|
|
735
|
+
odt_toast_duration_value: duration,
|
|
736
|
+
action: "mouseenter->odt-toast#pauseTimer mouseleave->odt-toast#resumeTimer"
|
|
737
|
+
}.merge(html_options.delete(:data) || {})
|
|
738
|
+
|
|
739
|
+
opts = html_options.merge(
|
|
740
|
+
class: classes.compact.reject(&:blank?).join(" "),
|
|
741
|
+
role: "status",
|
|
742
|
+
data: data_attrs
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
parts = []
|
|
746
|
+
if icon_name.present?
|
|
747
|
+
icon_classes = ["odt-toast__icon", "odt-toast__icon--#{variant_key}"]
|
|
748
|
+
parts << tag.span(render_icon_element(icon_name), class: icon_classes.join(" "))
|
|
749
|
+
end
|
|
750
|
+
|
|
751
|
+
body_content = block_given? ? capture(&block) : message
|
|
752
|
+
|
|
753
|
+
parts << tag.div(class: "odt-toast__content") do
|
|
754
|
+
c_parts = []
|
|
755
|
+
c_parts << tag.p(title, class: "odt-toast__title") if title.present?
|
|
756
|
+
c_parts << tag.p(body_content, class: "odt-toast__message") if body_content.present?
|
|
757
|
+
safe_join(c_parts)
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
if dismissible
|
|
761
|
+
parts << tag.button(
|
|
762
|
+
"×".html_safe,
|
|
763
|
+
type: "button",
|
|
764
|
+
class: "odt-toast__close",
|
|
765
|
+
aria: { label: "Close" },
|
|
766
|
+
data: { action: "click->odt-toast#dismiss" }
|
|
767
|
+
)
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
inner_content = safe_join(parts.compact)
|
|
771
|
+
tag.div(inner_content, **opts)
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
# Renders all active flash messages as ODT Toasts.
|
|
775
|
+
#
|
|
776
|
+
# @param flash_hash [ActionDispatch::Flash::FlashHash, Hash] Flash messages hash (default: flash)
|
|
777
|
+
# @param duration [Integer] Auto-dismiss duration in milliseconds
|
|
778
|
+
# @return [ActiveSupport::SafeBuffer]
|
|
779
|
+
def odt_flash_toasts(flash_hash = flash, duration: 4000)
|
|
780
|
+
return "".html_safe if flash_hash.blank?
|
|
781
|
+
|
|
782
|
+
toasts = []
|
|
783
|
+
flash_hash.each do |type, message|
|
|
784
|
+
next if message.blank?
|
|
785
|
+
|
|
786
|
+
variant = case type.to_sym
|
|
787
|
+
when :notice, :success
|
|
788
|
+
:success
|
|
789
|
+
when :alert, :error, :danger
|
|
790
|
+
:danger
|
|
791
|
+
when :warning
|
|
792
|
+
:warning
|
|
793
|
+
else
|
|
794
|
+
:info
|
|
795
|
+
end
|
|
796
|
+
|
|
797
|
+
toasts << odt_toast(message, variant: variant, duration: duration)
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
safe_join(toasts)
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
# Generates a Turbo Stream action to render a toast asynchronously.
|
|
804
|
+
#
|
|
805
|
+
# @param message [String, nil]
|
|
806
|
+
# @param title [String, nil]
|
|
807
|
+
# @param variant [:success, :warning, :danger, :error, :info]
|
|
808
|
+
# @param target [String] Target container ID (default: "odt-toasts")
|
|
809
|
+
# @param action [:append, :prepend] Turbo stream action (default: :append)
|
|
810
|
+
# @param duration [Integer] Auto-dismiss duration in ms (default: 4000)
|
|
811
|
+
# @return [ActiveSupport::SafeBuffer]
|
|
812
|
+
def odt_toast_stream(message = nil, title: nil, variant: :success, target: "odt-toasts", action: :append, duration: 4000, **options)
|
|
813
|
+
toast_html = odt_toast(message, title: title, variant: variant, duration: duration, **options)
|
|
814
|
+
|
|
815
|
+
if respond_to?(:turbo_stream)
|
|
816
|
+
turbo_stream.public_send(action, target, toast_html)
|
|
817
|
+
else
|
|
818
|
+
tag.public_send("turbo-stream", action: action, target: target) do
|
|
819
|
+
tag.template(toast_html)
|
|
820
|
+
end
|
|
821
|
+
end
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
# Renders an ODT Modal dialog container with Stimulus controller.
|
|
825
|
+
#
|
|
826
|
+
# @param id [String, nil] Unique HTML ID of the modal
|
|
827
|
+
# @param title [String, nil] Header title text
|
|
828
|
+
# @param subtitle [String, nil] Header subtitle text
|
|
829
|
+
# @param size [:sm, :md, :lg, :xl, :full] Dialog width size (default: :md)
|
|
830
|
+
# @param radius [:none, :xs, :sm, :md, :lg, :xl, :"2xl", :"3xl", :"4xl", :full, nil]
|
|
831
|
+
# @param dismissible [Boolean] Whether to render the header close button (default: true)
|
|
832
|
+
# @param open [Boolean] Whether modal is initially open (default: false)
|
|
833
|
+
# @param prevent_backdrop_close [Boolean] Prevent closing when clicking overlay (default: false)
|
|
834
|
+
# @return [ActiveSupport::SafeBuffer]
|
|
835
|
+
def odt_modal(id: nil, title: nil, subtitle: nil, size: :md, radius: :"2xl", dismissible: true, open: false, prevent_backdrop_close: false, **html_options, &block)
|
|
836
|
+
size_key = size.to_s.tr("_", "-")
|
|
837
|
+
radius_key = radius.to_s.tr("_", "-")
|
|
838
|
+
|
|
839
|
+
overlay_classes = [
|
|
840
|
+
"odt-modal-overlay",
|
|
841
|
+
("hidden" unless open),
|
|
842
|
+
html_options.delete(:class)
|
|
843
|
+
]
|
|
844
|
+
|
|
845
|
+
data_attrs = {
|
|
846
|
+
controller: "odt-modal",
|
|
847
|
+
odt_modal_open_value: open,
|
|
848
|
+
odt_modal_prevent_backdrop_close_value: prevent_backdrop_close,
|
|
849
|
+
action: "click->odt-modal#backdropClose keydown.esc@window->odt-modal#close"
|
|
850
|
+
}.merge(html_options.delete(:data) || {})
|
|
851
|
+
|
|
852
|
+
opts = html_options.merge(
|
|
853
|
+
id: id,
|
|
854
|
+
class: overlay_classes.compact.reject(&:blank?).join(" "),
|
|
855
|
+
role: "dialog",
|
|
856
|
+
aria: { modal: "true", hidden: (!open).to_s },
|
|
857
|
+
data: data_attrs
|
|
858
|
+
)
|
|
859
|
+
|
|
860
|
+
modal_classes = [
|
|
861
|
+
"odt-modal",
|
|
862
|
+
"odt-modal--#{size_key}",
|
|
863
|
+
"odt-radius-#{radius_key}"
|
|
864
|
+
]
|
|
865
|
+
|
|
866
|
+
body_content = block_given? ? capture(&block) : nil
|
|
867
|
+
|
|
868
|
+
tag.div(**opts) do
|
|
869
|
+
tag.div(class: modal_classes.join(" "), data: { odt_modal_target: "dialog" }) do
|
|
870
|
+
parts = []
|
|
871
|
+
if title.present? || subtitle.present? || dismissible
|
|
872
|
+
parts << odt_modal_header(title: title, subtitle: subtitle, dismissible: dismissible)
|
|
873
|
+
end
|
|
874
|
+
|
|
875
|
+
if body_content.present?
|
|
876
|
+
if body_content.include?("odt-modal__body") || body_content.include?("odt-modal__header")
|
|
877
|
+
parts << body_content
|
|
878
|
+
else
|
|
879
|
+
parts << tag.div(body_content, class: "odt-modal__body")
|
|
880
|
+
end
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
safe_join(parts)
|
|
884
|
+
end
|
|
885
|
+
end
|
|
886
|
+
end
|
|
887
|
+
|
|
888
|
+
# Renders an ODT Modal Header subcomponent.
|
|
889
|
+
#
|
|
890
|
+
# @param title [String, nil]
|
|
891
|
+
# @param subtitle [String, nil]
|
|
892
|
+
# @param dismissible [Boolean]
|
|
893
|
+
# @return [ActiveSupport::SafeBuffer]
|
|
894
|
+
def odt_modal_header(title: nil, subtitle: nil, dismissible: true, **html_options, &block)
|
|
895
|
+
classes = ["odt-modal__header", html_options.delete(:class)]
|
|
896
|
+
opts = html_options.merge(class: classes.compact.reject(&:blank?).join(" "))
|
|
897
|
+
|
|
898
|
+
tag.div(**opts) do
|
|
899
|
+
parts = []
|
|
900
|
+
if block_given?
|
|
901
|
+
parts << capture(&block)
|
|
902
|
+
else
|
|
903
|
+
parts << tag.div(class: "odt-modal__header-content") do
|
|
904
|
+
c_parts = []
|
|
905
|
+
c_parts << tag.h3(title, class: "odt-modal__title") if title.present?
|
|
906
|
+
c_parts << tag.p(subtitle, class: "odt-modal__subtitle") if subtitle.present?
|
|
907
|
+
safe_join(c_parts)
|
|
908
|
+
end
|
|
909
|
+
end
|
|
910
|
+
|
|
911
|
+
if dismissible
|
|
912
|
+
parts << tag.button(
|
|
913
|
+
"×".html_safe,
|
|
914
|
+
type: "button",
|
|
915
|
+
class: "odt-modal__close",
|
|
916
|
+
aria: { label: "Close" },
|
|
917
|
+
data: { action: "click->odt-modal#close" }
|
|
918
|
+
)
|
|
919
|
+
end
|
|
920
|
+
|
|
921
|
+
safe_join(parts)
|
|
922
|
+
end
|
|
923
|
+
end
|
|
924
|
+
|
|
925
|
+
# Renders an ODT Modal Body subcomponent.
|
|
926
|
+
#
|
|
927
|
+
# @return [ActiveSupport::SafeBuffer]
|
|
928
|
+
def odt_modal_body(**html_options, &block)
|
|
929
|
+
classes = ["odt-modal__body", html_options.delete(:class)]
|
|
930
|
+
opts = html_options.merge(class: classes.compact.reject(&:blank?).join(" "))
|
|
931
|
+
content = block_given? ? capture(&block) : nil
|
|
932
|
+
tag.div(content, **opts)
|
|
933
|
+
end
|
|
934
|
+
|
|
935
|
+
# Renders an ODT Modal Footer subcomponent.
|
|
936
|
+
#
|
|
937
|
+
# @return [ActiveSupport::SafeBuffer]
|
|
938
|
+
def odt_modal_footer(**html_options, &block)
|
|
939
|
+
classes = ["odt-modal__footer", html_options.delete(:class)]
|
|
940
|
+
opts = html_options.merge(class: classes.compact.reject(&:blank?).join(" "))
|
|
941
|
+
content = block_given? ? capture(&block) : nil
|
|
942
|
+
tag.div(content, **opts)
|
|
943
|
+
end
|
|
944
|
+
|
|
680
945
|
private
|
|
681
946
|
|
|
682
947
|
# Normalizes button variant key with backwards compatibility
|
data/lib/odt_ui/version.rb
CHANGED
data/sig/odt_ui.rbs
CHANGED
|
@@ -216,6 +216,78 @@ module OdtUi
|
|
|
216
216
|
?required: bool,
|
|
217
217
|
**untyped
|
|
218
218
|
) ?{ () -> untyped } -> untyped
|
|
219
|
+
|
|
220
|
+
type toaster_position = :top_right | :top_center | :top_left | :bottom_right | :bottom_center | :bottom_left | Symbol | String
|
|
221
|
+
type toast_variant = :success | :warning | :danger | :error | :info | :loading | :frosted | Symbol | String
|
|
222
|
+
|
|
223
|
+
# Renders an ODT Toaster viewport container.
|
|
224
|
+
def odt_toaster: (
|
|
225
|
+
?position: toaster_position,
|
|
226
|
+
?id: String,
|
|
227
|
+
**untyped
|
|
228
|
+
) ?{ () -> untyped } -> untyped
|
|
229
|
+
|
|
230
|
+
# Renders a single ODT Toast card.
|
|
231
|
+
def odt_toast: (
|
|
232
|
+
?String? message,
|
|
233
|
+
?title: String?,
|
|
234
|
+
?variant: toast_variant,
|
|
235
|
+
?icon: String?,
|
|
236
|
+
?duration: Integer,
|
|
237
|
+
?dismissible: bool,
|
|
238
|
+
?radius: radius_size?,
|
|
239
|
+
**untyped
|
|
240
|
+
) ?{ () -> untyped } -> untyped
|
|
241
|
+
|
|
242
|
+
# Renders active flash messages as ODT Toasts.
|
|
243
|
+
def odt_flash_toasts: (
|
|
244
|
+
?untyped flash_hash,
|
|
245
|
+
?duration: Integer
|
|
246
|
+
) -> untyped
|
|
247
|
+
|
|
248
|
+
# Generates a Turbo Stream action for a toast.
|
|
249
|
+
def odt_toast_stream: (
|
|
250
|
+
?String? message,
|
|
251
|
+
?title: String?,
|
|
252
|
+
?variant: toast_variant,
|
|
253
|
+
?target: String,
|
|
254
|
+
?action: Symbol | String,
|
|
255
|
+
?duration: Integer,
|
|
256
|
+
**untyped
|
|
257
|
+
) -> untyped
|
|
258
|
+
|
|
259
|
+
type modal_size = :sm | :md | :lg | :xl | :full | Symbol | String
|
|
260
|
+
|
|
261
|
+
# Renders an ODT Modal dialog container.
|
|
262
|
+
def odt_modal: (
|
|
263
|
+
?id: String?,
|
|
264
|
+
?title: String?,
|
|
265
|
+
?subtitle: String?,
|
|
266
|
+
?size: modal_size,
|
|
267
|
+
?radius: radius_size?,
|
|
268
|
+
?dismissible: bool,
|
|
269
|
+
?open: bool,
|
|
270
|
+
?prevent_backdrop_close: bool,
|
|
271
|
+
**untyped
|
|
272
|
+
) ?{ () -> untyped } -> untyped
|
|
273
|
+
|
|
274
|
+
# Renders an ODT Modal Header subcomponent.
|
|
275
|
+
def odt_modal_header: (
|
|
276
|
+
?title: String?,
|
|
277
|
+
?subtitle: String?,
|
|
278
|
+
?dismissible: bool,
|
|
279
|
+
**untyped
|
|
280
|
+
) ?{ () -> untyped } -> untyped
|
|
281
|
+
|
|
282
|
+
# Renders an ODT Modal Body subcomponent.
|
|
283
|
+
def odt_modal_body: (
|
|
284
|
+
**untyped
|
|
285
|
+
) ?{ () -> untyped } -> untyped
|
|
286
|
+
|
|
287
|
+
# Renders an ODT Modal Footer subcomponent.
|
|
288
|
+
def odt_modal_footer: (
|
|
289
|
+
**untyped
|
|
290
|
+
) ?{ () -> untyped } -> untyped
|
|
219
291
|
end
|
|
220
292
|
end
|
|
221
293
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: odt-ui-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.2.
|
|
4
|
+
version: 2.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Pixel
|
|
@@ -29,7 +29,164 @@ dependencies:
|
|
|
29
29
|
- - "<"
|
|
30
30
|
- !ruby/object:Gem::Version
|
|
31
31
|
version: '9'
|
|
32
|
-
description:
|
|
32
|
+
description: "<div align=\"center\">\n\n# ODT UI for Ruby on Rails (`odt-ui-rails`)\n\n**Standalone
|
|
33
|
+
CSS-first design tokens, Hotwire-ready UI components, and ActionView helpers for
|
|
34
|
+
Ruby on Rails.**\n\n[](https://badge.fury.io/rb/odt-ui-rails)\n[](LICENSE)\n[](https://www.ruby-lang.org)\n[](https://rubyonrails.org)\n[](https://hotwired.dev)\n[](sig/odt_ui.rbs)\n\n</div>\n\n---\n\n##
|
|
38
|
+
Overview\n\n`odt-ui-rails` brings the **ODT Design System** directly into Ruby on
|
|
39
|
+
Rails. It is architected from the ground up to provide a **Standalone CSS First**
|
|
40
|
+
experience:\n\n- \U0001F4E6 **Zero Host Build Tools Required:** Ships pre-compiled
|
|
41
|
+
CSS custom properties and `.odt-*` component styles. Works out-of-the-box with Propshaft,
|
|
42
|
+
Sprockets, or no asset pipeline at all.\n- ⚡ **Seamless Tailwind CSS v4 Interoperability:**
|
|
43
|
+
Includes a complete `@theme` token configuration for Tailwind v4 projects, allowing
|
|
44
|
+
custom utility classes (`class: \"...\"`) to merge effortlessly onto components.\n-
|
|
45
|
+
\U0001F680 **Hotwire & Turbo Native:** First-class helpers for Turbo Streams (`odt_toast_stream`),
|
|
46
|
+
Flash message auto-toasts (`odt_flash_toasts`), and Stimulus-powered Modals (`odt_modal`).\n-
|
|
47
|
+
\U0001F48E **Strict Type Signatures:** Full RBS definitions included for Ruby LSP,
|
|
48
|
+
Steep, and Solargraph IDE autocompletion.\n\n---\n\n## Table of Contents\n\n- [Installation](#installation)\n-
|
|
49
|
+
[Asset Pipeline Configuration](#asset-pipeline-configuration)\n- [Component Reference](#component-reference)\n
|
|
50
|
+
\ - [Feedback & Overlays](#feedback--overlays)\n - [Toasts & Toaster](#toasts--toaster)\n
|
|
51
|
+
\ - [Modals](#modals)\n - [Alerts](#alerts)\n - [Actions & Indicators](#actions--indicators)\n
|
|
52
|
+
\ - [Buttons](#buttons)\n - [Badges](#badges)\n - [Form Controls](#form-controls)\n
|
|
53
|
+
\ - [Input Field](#input-field)\n - [Textarea](#textarea)\n - [Select Dropdown](#select-dropdown)\n
|
|
54
|
+
\ - [Checkbox & Radio](#checkbox--radio)\n - [Form Group](#form-group)\n -
|
|
55
|
+
[Layout & Typography](#layout--typography)\n - [Cards](#cards)\n - [Typography
|
|
56
|
+
(Heading & Text)](#typography-heading--text)\n - [Avatars](#avatars)\n- [Theming
|
|
57
|
+
& Dark Mode](#theming--dark-mode)\n- [License](#license)\n\n---\n\n## Installation\n\nAdd
|
|
58
|
+
the gem to your application's `Gemfile`:\n\n```ruby\ngem \"odt-ui-rails\", \"~>
|
|
59
|
+
2.2.0\"\n```\n\nInstall dependencies:\n\n```bash\nbundle install\n```\n\nRun the
|
|
60
|
+
automated install generator:\n\n```bash\nbin/rails g odt:install\n```\n\nThe generator
|
|
61
|
+
will:\n\n1. Detect your asset pipeline (Tailwind CSS v4, Propshaft, or Sprockets)
|
|
62
|
+
and safely configure the required `@import` statements.\n2. Copy `sig/odt_ui.rbs`
|
|
63
|
+
for Ruby LSP IDE autocompletion.\n3. Copy Stimulus controllers (`odt_toast_controller.js`,
|
|
64
|
+
`odt_modal_controller.js`) to `app/javascript/controllers/` if Stimulus is present.\n\n---\n\n##
|
|
65
|
+
Asset Pipeline Configuration\n\nIf configuring stylesheets manually, use the configuration
|
|
66
|
+
below:\n\n### Option A: Tailwind CSS v4 (`app/assets/tailwind/application.css`)\n\n```css\n@import
|
|
67
|
+
\"tailwindcss\";\n@import \"odt_ui/tokens.css\";\n@import \"odt_ui/styles.css\";\n@import
|
|
68
|
+
\"odt_ui/theme.css\";\n```\n\n### Option B: Propshaft / Sprockets / Standard Rails
|
|
69
|
+
(`app/assets/stylesheets/application.css`)\n\n```css\n@import \"odt_ui/tokens.css\";\n@import
|
|
70
|
+
\"odt_ui/styles.css\";\n```\n\n---\n\n## Component Reference\n\nAll helpers are
|
|
71
|
+
globally available in all ERB templates, layouts, and helpers.\n\n---\n\n### Feedback
|
|
72
|
+
& Overlays\n\n#### Toasts & Toaster\n\nProvides viewport stacking containers, automated
|
|
73
|
+
Flash message toasts on redirects, and Turbo Stream responses for async form actions.\n\n```erb\n<%#
|
|
74
|
+
In app/views/layouts/application.html.erb %>\n<body class=\"bg-surface text-fg font-sans
|
|
75
|
+
antialiased\">\n <%= odt_toaster position: :top_right do %>\n <%= odt_flash_toasts
|
|
76
|
+
%>\n <% end %>\n\n <%= yield %>\n</body>\n```\n\n**Controller Turbo Stream Action:**\n\n```ruby\ndef
|
|
77
|
+
create\n @todo = Todo.new(todo_params)\n if @todo.save\n respond_to do |format|\n
|
|
78
|
+
\ format.turbo_stream do\n render turbo_stream: [\n turbo_stream.prepend(\"todos\",
|
|
79
|
+
@todo),\n odt_toast_stream(\"Todo created successfully!\", variant: :success)\n
|
|
80
|
+
\ ]\n end\n format.html { redirect_to todos_path, notice: \"Todo
|
|
81
|
+
created successfully!\" }\n end\n end\nend\n```\n\n**Individual Toast Helper:**\n\n```erb\n<%=
|
|
82
|
+
odt_toast \"Settings updated\", title: \"Saved\", variant: :success, duration: 4000
|
|
83
|
+
%>\n<%= odt_toast \"Failed to sync records\", variant: :danger %>\n```\n\n| Parameter
|
|
84
|
+
\ | Type | Default | Description |\n|
|
|
85
|
+
:------------ | :-------- | :--------- | :---------------------------------------------------------------------------
|
|
86
|
+
|\n| `message` | `String` | `nil` | Toast description or body text |\n|
|
|
87
|
+
`title` | `String` | `nil` | Optional bold header title |\n|
|
|
88
|
+
`variant` | `Symbol` | `:success` | `:success`, `:warning`, `:danger`, `:error`,
|
|
89
|
+
`:info`, `:loading`, `:frosted` |\n| `duration` | `Integer` | `4000` | Auto-dismiss
|
|
90
|
+
duration in ms (`0` to disable) |\n| `dismissible`
|
|
91
|
+
| `Boolean` | `true` | Renders close button (`×`) |\n\n---\n\n####
|
|
92
|
+
Modals\n\nAccessible dialog containers with backdrop overlay, ESC key handling,
|
|
93
|
+
scroll locking, and smooth scale transitions.\n\n```erb\n<%# Trigger Button %>\n<%=
|
|
94
|
+
odt_button \"New Task\", color: :primary, data: { action: \"click->odt-modal#open\"
|
|
95
|
+
} %>\n\n<%# Standard Modal %>\n<%= odt_modal id: \"new_task_modal\", title: \"Create
|
|
96
|
+
Task\", subtitle: \"Add a task to your board\", size: :md do %>\n <%= render \"form\",
|
|
97
|
+
todo: Todo.new %>\n<% end %>\n\n<%# Custom Composition Modal %>\n<%= odt_modal id:
|
|
98
|
+
\"confirm_delete_modal\", size: :sm do %>\n <%= odt_modal_header title: \"Delete
|
|
99
|
+
Item?\", subtitle: \"This action cannot be undone.\" %>\n <%= odt_modal_body do
|
|
100
|
+
%>\n <p class=\"text-sm text-fg-muted\">Are you sure you want to permanently
|
|
101
|
+
remove this record?</p>\n <% end %>\n <%= odt_modal_footer do %>\n <%= odt_button
|
|
102
|
+
\"Cancel\", variant: :ghost, data: { action: \"click->odt-modal#close\" } %>\n <%=
|
|
103
|
+
odt_button \"Delete\", color: :danger, data: { turbo_method: :delete } %>\n <%
|
|
104
|
+
end %>\n<% end %>\n```\n\n| Parameter | Type | Default | Description
|
|
105
|
+
\ |\n| :----------------------- | :-------- | :-------
|
|
106
|
+
| :------------------------------------------- |\n| `id` | `String`
|
|
107
|
+
\ | `nil` | HTML identifier for Stimulus/Turbo targeting |\n| `title` / `subtitle`
|
|
108
|
+
\ | `String` | `nil` | Header titles |\n|
|
|
109
|
+
`size` | `Symbol` | `:md` | `:sm`, `:md`, `:lg`, `:xl`, `:full`
|
|
110
|
+
\ |\n| `radius` | `Symbol` | `:\"2xl\"` | `:none` to `:\"4xl\"`,
|
|
111
|
+
`:full` |\n| `dismissible` | `Boolean` | `true` | Renders
|
|
112
|
+
close button |\n| `open` | `Boolean` |
|
|
113
|
+
`false` | Initial visible state |\n| `prevent_backdrop_close`
|
|
114
|
+
| `Boolean` | `false` | Disables closing on overlay click |\n\n---\n\n####
|
|
115
|
+
Alerts\n\nInline banners for status notices, warning messages, and form callouts.\n\n```erb\n<%=
|
|
116
|
+
odt_alert \"Your subscription will renew on May 1st.\", variant: :info %>\n<%= odt_alert
|
|
117
|
+
\"Payment failed. Please update billing info.\", variant: :danger, icon: \"fa-solid
|
|
118
|
+
fa-triangle-exclamation\" %>\n```\n\n---\n\n### Actions & Indicators\n\n#### Buttons\n\nVersatile
|
|
119
|
+
action buttons supporting brand colors, frosted glass surfaces, loading states,
|
|
120
|
+
and icon decorations.\n\n```erb\n<%# Standard Brand Action %>\n<%= odt_button \"Save
|
|
121
|
+
Changes\", color: :primary, variant: :filled, radius: :full %>\n\n<%# Secondary
|
|
122
|
+
& Frosted Glass %>\n<%= odt_button \"Secondary\", variant: :subtle, color: :secondary
|
|
123
|
+
%>\n<%= odt_button \"Frosted Glass\", variant: :frosted, color: :primary %>\n\n<%#
|
|
124
|
+
Link as Button %>\n<%= odt_button \"View Project\", href: project_path(@project),
|
|
125
|
+
variant: :subtle %>\n\n<%# Loading & Icons %>\n<%= odt_button \"Add Task\", icon:
|
|
126
|
+
\"fa-solid fa-plus\", color: :primary %>\n<%= odt_button \"Saving...\", loading:
|
|
127
|
+
true %>\n<%= odt_button icon: \"fa-solid fa-trash\", icon_only: true, color: :danger,
|
|
128
|
+
variant: :ghost %>\n```\n\n| Option | Type | Default | Description
|
|
129
|
+
\ |\n|
|
|
130
|
+
:-------------------- | :-------- | :--------- | :-----------------------------------------------------------------------------------------------------------------
|
|
131
|
+
|\n| `variant` | `Symbol` | `:filled` | `:filled`, `:subtle`, `:ghost`,
|
|
132
|
+
`:outline`, `:capsule`, `:frosted`, `:light` |\n|
|
|
133
|
+
`color` | `Symbol` | `:primary` | `:primary`, `:secondary`, `:surface`,
|
|
134
|
+
`:muted`, `:inverse`, `:success`, `:warning`, `:danger`, `:info`, `:neutral` |\n|
|
|
135
|
+
`size` | `Symbol` | `:md` | `:xs`, `:sm`, `:md`, `:lg`, `:xl`
|
|
136
|
+
\ |\n|
|
|
137
|
+
`radius` | `Symbol` | `:\"2xl\"` | `:none`, `:xs`, `:sm`, `:md`,
|
|
138
|
+
`:lg`, `:xl`, `:\"2xl\"`, `:\"3xl\"`, `:\"4xl\"`, `:full` |\n|
|
|
139
|
+
`icon` / `right_icon` | `String` | `nil` | FontAwesome class or inline SVG
|
|
140
|
+
string |\n|
|
|
141
|
+
`icon_only` | `Boolean` | `false` | Square icon button formatting |\n|
|
|
142
|
+
`loading` | `Boolean` | `false` | Replaces icon with spinning loader
|
|
143
|
+
\ |\n|
|
|
144
|
+
`disabled` | `Boolean` | `false` | HTML disabled state |\n|
|
|
145
|
+
`full_width` | `Boolean` | `false` | Expands width to 100% |\n\n---\n\n####
|
|
146
|
+
Badges\n\nCompact status indicators with optional indicator dots and icons.\n\n```erb\n<%=
|
|
147
|
+
odt_badge \"Active\", variant: :subtle, color: :success, dot: true %>\n<%= odt_badge
|
|
148
|
+
\"High Priority\", variant: :filled, color: :danger, radius: :full %>\n<%= odt_badge
|
|
149
|
+
\"Calendar\", icon: \"fa-regular fa-calendar\", variant: :outlined %>\n```\n\n---\n\n###
|
|
150
|
+
Form Controls\n\nAccessible form components complete with labels, helper text, error
|
|
151
|
+
validations, icons, and input adornments.\n\n```erb\n<%= form_with model: @user,
|
|
152
|
+
class: \"space-y-4\" do |f| %>\n <%# Text Input with Left Icon %>\n <%= odt_input(\n
|
|
153
|
+
\ label: \"Email Address\",\n name: \"user[email]\",\n value: @user.email,\n
|
|
154
|
+
\ type: \"email\",\n placeholder: \"you@example.com\",\n left_icon: \"fa-solid
|
|
155
|
+
fa-envelope\",\n error: @user.errors[:email].first,\n required: true\n )
|
|
156
|
+
%>\n\n <%# Textarea %>\n <%= odt_textarea(\n label: \"Bio\",\n name: \"user[bio]\",\n
|
|
157
|
+
\ value: @user.bio,\n rows: 4,\n helper: \"Brief summary for your profile\"\n
|
|
158
|
+
\ ) %>\n\n <%# Select Dropdown %>\n <%= odt_select(\n label: \"Country\",\n
|
|
159
|
+
\ name: \"user[country]\",\n options: [[\"Thailand\", \"TH\"], [\"United States\",
|
|
160
|
+
\"US\"], [\"Japan\", \"JP\"]],\n selected: @user.country,\n prompt: \"Select
|
|
161
|
+
country\"\n ) %>\n\n <%# Checkbox %>\n <%= odt_checkbox(\n label: \"Subscribe
|
|
162
|
+
to newsletter\",\n description: \"Receive product updates once a week\",\n name:
|
|
163
|
+
\"user[subscribed]\",\n checked: true,\n color: :primary\n ) %>\n\n <%#
|
|
164
|
+
Radio Group %>\n <%= odt_radio label: \"Monthly Billing\", name: \"plan\", value:
|
|
165
|
+
\"monthly\", checked: true %>\n <%= odt_radio label: \"Annual Billing (Save 20%)\",
|
|
166
|
+
name: \"plan\", value: \"annual\" %>\n\n <%# Custom Slot Form Group %>\n <%= odt_form_group
|
|
167
|
+
label: \"Custom Input\", helper: \"Unique value required\" do %>\n <input type=\"text\"
|
|
168
|
+
class=\"odt-input odt-input--outlined odt-input--md\" name=\"custom\" />\n <% end
|
|
169
|
+
%>\n<% end %>\n```\n\n---\n\n### Layout & Typography\n\n#### Cards\n\nVersatile
|
|
170
|
+
containers supporting header metadata, multiple surface styles, and hover transitions.\n\n```erb\n<%=
|
|
171
|
+
odt_card title: \"Analytics Overview\", subtitle: \"Monthly performance\", variant:
|
|
172
|
+
:elevated, radius: :\"3xl\" do %>\n <p class=\"text-sm text-fg\">Dashboard charts
|
|
173
|
+
and statistics...</p>\n<% end %>\n\n<%= odt_card variant: :frosted, radius: :\"2xl\",
|
|
174
|
+
hoverable: true do %>\n <span>Interactive frosted glass card</span>\n<% end %>\n```\n\n####
|
|
175
|
+
Typography (`odt_heading` & `odt_text`)\n\nStandardized typographic elements mapped
|
|
176
|
+
to the design system scale.\n\n```erb\n<%= odt_heading \"Task Dashboard\", as: :h1,
|
|
177
|
+
size: :\"4xl\", weight: :bold, color: :strong %>\n<%= odt_heading \"Section Title\",
|
|
178
|
+
as: :h3, size: :xl %>\n\n<%= odt_text \"Manage all project deliverables in one workspace.\",
|
|
179
|
+
size: :sm, color: :muted %>\n<%= odt_text \"A very long single-line description...\",
|
|
180
|
+
truncate: true %>\n```\n\n#### Avatars\n\nRenders image avatars or automatically
|
|
181
|
+
computes two-letter initials from full names.\n\n```erb\n<%# Generates initials
|
|
182
|
+
avatar \"JD\" %>\n<%= odt_avatar name: \"John Doe\", size: :lg, color: :secondary
|
|
183
|
+
%>\n\n<%# Image avatar with fallback %>\n<%= odt_avatar src: \"https://avatar.vercel.sh/pixel\",
|
|
184
|
+
size: :md %>\n```\n\n---\n\n## Theming & Dark Mode\n\nThe design system is fully
|
|
185
|
+
dark mode ready. Apply `data-theme=\"dark\"` or the `.dark` class to `<html>` or
|
|
186
|
+
any sub-container:\n\n```html\n<html data-theme=\"dark\" class=\"dark\">\n ...\n</html>\n```\n\nAll
|
|
187
|
+
design tokens (backgrounds, surfaces, borders, text, and shadows) adapt automatically
|
|
188
|
+
without custom CSS.\n\n---\n\n## License\n\nThe gem is available as open source
|
|
189
|
+
under the terms of the [MIT License](LICENSE).\n"
|
|
33
190
|
email:
|
|
34
191
|
- pixxel.tk@gmail.com
|
|
35
192
|
executables: []
|
|
@@ -41,7 +198,11 @@ files:
|
|
|
41
198
|
- app/assets/stylesheets/odt_ui/styles.css
|
|
42
199
|
- app/assets/stylesheets/odt_ui/theme.css
|
|
43
200
|
- app/assets/stylesheets/odt_ui/tokens.css
|
|
201
|
+
- app/javascript/controllers/odt_modal_controller.js
|
|
202
|
+
- app/javascript/controllers/odt_toast_controller.js
|
|
44
203
|
- lib/generators/odt/install_generator.rb
|
|
204
|
+
- lib/generators/odt/templates/odt_modal_controller.js
|
|
205
|
+
- lib/generators/odt/templates/odt_toast_controller.js
|
|
45
206
|
- lib/odt-ui-rails.rb
|
|
46
207
|
- lib/odt_ui/engine.rb
|
|
47
208
|
- lib/odt_ui/helper.rb
|
|
@@ -50,7 +211,11 @@ files:
|
|
|
50
211
|
homepage: https://github.com/PixxelTK/ODT-Lightweight-UI
|
|
51
212
|
licenses:
|
|
52
213
|
- MIT
|
|
53
|
-
metadata:
|
|
214
|
+
metadata:
|
|
215
|
+
homepage_uri: https://github.com/PixxelTK/ODT-Lightweight-UI
|
|
216
|
+
source_code_uri: https://github.com/PixxelTK/ODT-Lightweight-UI/tree/main/integrations/rails
|
|
217
|
+
documentation_uri: https://github.com/PixxelTK/ODT-Lightweight-UI/blob/main/integrations/rails/README.md
|
|
218
|
+
changelog_uri: https://github.com/PixxelTK/ODT-Lightweight-UI/releases
|
|
54
219
|
rdoc_options: []
|
|
55
220
|
require_paths:
|
|
56
221
|
- lib
|
|
@@ -67,5 +232,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
67
232
|
requirements: []
|
|
68
233
|
rubygems_version: 3.6.9
|
|
69
234
|
specification_version: 4
|
|
70
|
-
summary:
|
|
235
|
+
summary: Standalone CSS design tokens, Hotwire components, and ActionView helpers
|
|
236
|
+
for Ruby on Rails
|
|
71
237
|
test_files: []
|