cafe_car 0.1.0 → 0.1.2

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 (240) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +682 -8
  3. data/Rakefile +21 -0
  4. data/app/assets/fonts/Lexend.css +7 -0
  5. data/app/assets/fonts/Lexend.ttf +0 -0
  6. data/app/assets/images/noise.svg +16 -0
  7. data/app/assets/stylesheets/actiontext.css +31 -0
  8. data/app/assets/stylesheets/application.css +1 -0
  9. data/app/assets/stylesheets/cafe_car/code/base16-dark.css +89 -0
  10. data/app/assets/stylesheets/cafe_car/code/base16-light.css +90 -0
  11. data/app/assets/stylesheets/cafe_car/pagination.css +5 -0
  12. data/app/assets/stylesheets/cafe_car/themes/cool.css +32 -0
  13. data/app/assets/stylesheets/cafe_car/themes/cool2.css +31 -0
  14. data/app/assets/stylesheets/cafe_car/themes/defaults.css +60 -0
  15. data/app/assets/stylesheets/cafe_car/themes/warm-dark.css +29 -0
  16. data/app/assets/stylesheets/cafe_car/themes/warm.css +24 -0
  17. data/app/assets/stylesheets/cafe_car/tooltips.css +20 -0
  18. data/app/assets/stylesheets/cafe_car/trix.css +56 -0
  19. data/app/assets/stylesheets/cafe_car/utility.css +63 -0
  20. data/app/assets/stylesheets/cafe_car.css +96 -0
  21. data/app/assets/stylesheets/iconoir.css +22 -0
  22. data/app/assets/stylesheets/ui/Alert.css +25 -0
  23. data/app/assets/stylesheets/ui/Article.css +11 -0
  24. data/app/assets/stylesheets/ui/Button.css +42 -0
  25. data/app/assets/stylesheets/ui/Card.css +74 -0
  26. data/app/assets/stylesheets/ui/Chat.css +33 -0
  27. data/app/assets/stylesheets/ui/Close.css +11 -0
  28. data/app/assets/stylesheets/ui/Code.css +4 -0
  29. data/app/assets/stylesheets/ui/Controls.css +16 -0
  30. data/app/assets/stylesheets/ui/Error.css +3 -0
  31. data/app/assets/stylesheets/ui/Example.css +45 -0
  32. data/app/assets/stylesheets/ui/Field.css +31 -0
  33. data/app/assets/stylesheets/ui/Grid.css +6 -0
  34. data/app/assets/stylesheets/ui/Group.css +16 -0
  35. data/app/assets/stylesheets/ui/Icon.css +27 -0
  36. data/app/assets/stylesheets/ui/Image.css +14 -0
  37. data/app/assets/stylesheets/ui/InfoCircle.css +11 -0
  38. data/app/assets/stylesheets/ui/Input.css +36 -0
  39. data/app/assets/stylesheets/ui/Layout.css +100 -0
  40. data/app/assets/stylesheets/ui/Menu.css +38 -0
  41. data/app/assets/stylesheets/ui/Modal.css +26 -0
  42. data/app/assets/stylesheets/ui/Navigation.css +37 -0
  43. data/app/assets/stylesheets/ui/Page.css +105 -0
  44. data/app/assets/stylesheets/ui/Row.css +9 -0
  45. data/app/assets/stylesheets/ui/Table.css +101 -0
  46. data/app/assets/stylesheets/ui/components.css +24 -0
  47. data/app/controllers/cafe_car/application_controller.rb +9 -0
  48. data/app/controllers/cafe_car/examples_controller.rb +22 -0
  49. data/app/controllers/cafe_car/sessions_controller.rb +30 -0
  50. data/app/controllers/concerns/cafe_car/authentication.rb +61 -0
  51. data/app/javascript/application.js +5 -0
  52. data/app/javascript/cafe_car.js +174 -0
  53. data/app/models/cafe_car/session.rb +18 -0
  54. data/app/policies/cafe_car/application_policy.rb +42 -0
  55. data/app/policies/cafe_car/session_policy.rb +19 -0
  56. data/app/presenters/cafe_car/action_text/rich_text_presenter.rb +7 -0
  57. data/app/presenters/cafe_car/active_record/base_presenter.rb +6 -0
  58. data/app/presenters/cafe_car/active_record/relation_presenter.rb +17 -0
  59. data/app/presenters/cafe_car/active_storage/attached/one_presenter.rb +9 -0
  60. data/app/presenters/cafe_car/active_storage/attachment_presenter.rb +18 -0
  61. data/app/presenters/cafe_car/basic_object_presenter.rb +5 -0
  62. data/app/presenters/cafe_car/code_presenter.rb +18 -0
  63. data/app/presenters/cafe_car/currency_presenter.rb +5 -0
  64. data/app/presenters/cafe_car/date_and_time/compatibility_presenter.rb +6 -0
  65. data/app/presenters/cafe_car/date_presenter.rb +5 -0
  66. data/app/presenters/cafe_car/date_time_presenter.rb +11 -0
  67. data/app/presenters/cafe_car/enumerable_presenter.rb +13 -0
  68. data/app/presenters/cafe_car/false_class_presenter.rb +5 -0
  69. data/app/presenters/cafe_car/hash_presenter.rb +6 -0
  70. data/app/presenters/cafe_car/nil_class_presenter.rb +13 -0
  71. data/app/presenters/cafe_car/presenter.rb +157 -0
  72. data/app/presenters/cafe_car/range_presenter.rb +16 -0
  73. data/app/presenters/cafe_car/record_presenter.rb +5 -0
  74. data/app/presenters/cafe_car/string_presenter.rb +20 -0
  75. data/app/presenters/cafe_car/symbol_presenter.rb +5 -0
  76. data/app/presenters/cafe_car/true_class_presenter.rb +5 -0
  77. data/app/ui/cafe_car/ui/button.rb +9 -0
  78. data/app/ui/cafe_car/ui/card.rb +18 -0
  79. data/app/ui/cafe_car/ui/field.rb +11 -0
  80. data/app/ui/cafe_car/ui/grid.rb +30 -0
  81. data/app/ui/cafe_car/ui/layout.rb +7 -0
  82. data/app/ui/cafe_car/ui/page.rb +14 -0
  83. data/app/views/application/_actions.html.haml +1 -0
  84. data/app/views/application/_alerts.html.haml +2 -0
  85. data/app/views/application/_body.html.haml +7 -0
  86. data/app/views/application/_controls.html.haml +12 -0
  87. data/app/views/application/_debug.html.haml +18 -0
  88. data/app/views/application/_empty.html.haml +1 -0
  89. data/app/views/application/_errors.html.haml +4 -0
  90. data/app/views/application/_field.html.haml +5 -0
  91. data/app/views/application/_fields.html.haml +1 -0
  92. data/app/views/application/_filters.html.haml +8 -0
  93. data/app/views/application/_form.html.haml +6 -0
  94. data/app/views/application/_grid.html.haml +3 -0
  95. data/app/views/application/_grid_item.html.haml +1 -0
  96. data/app/views/application/_head.html.haml +17 -0
  97. data/app/views/application/_index.html.haml +8 -0
  98. data/app/views/application/_index_actions.html.haml +7 -0
  99. data/app/views/application/_navigation.html.haml +9 -0
  100. data/app/views/application/_navigation_links.html.haml +5 -0
  101. data/app/views/application/_notes.html.haml +10 -0
  102. data/app/views/application/_popup.html.haml +7 -0
  103. data/app/views/application/_show.html.haml +9 -0
  104. data/app/views/application/_submit.html.haml +1 -0
  105. data/app/views/application/_table.html.haml +6 -0
  106. data/app/views/cafe_car/application/create.turbo_stream.haml +2 -0
  107. data/app/views/cafe_car/application/destroy.turbo_stream.haml +1 -0
  108. data/app/views/cafe_car/application/edit.html.haml +18 -0
  109. data/app/views/cafe_car/application/edit.turbo_stream.haml +7 -0
  110. data/app/views/cafe_car/application/index.html.haml +15 -0
  111. data/app/views/cafe_car/application/new.html.haml +8 -0
  112. data/app/views/cafe_car/application/new.turbo_stream.haml +8 -0
  113. data/app/views/cafe_car/application/show.html.haml +36 -0
  114. data/app/views/cafe_car/application/update.turbo_stream.haml +2 -0
  115. data/app/views/cafe_car/examples/_example.html.haml +12 -0
  116. data/app/views/cafe_car/examples/_index.html.haml +16 -0
  117. data/app/views/cafe_car/examples/_navigation_links.html.haml +2 -0
  118. data/app/views/cafe_car/examples/ui/_alert.html.haml +4 -0
  119. data/app/views/cafe_car/examples/ui/_button.html.haml +3 -0
  120. data/app/views/cafe_car/examples/ui/_card.html.haml +6 -0
  121. data/app/views/cafe_car/examples/ui/_chat.html.haml +3 -0
  122. data/app/views/cafe_car/examples/ui/_controls.html.haml +3 -0
  123. data/app/views/cafe_car/examples/ui/_error.html.haml +1 -0
  124. data/app/views/cafe_car/examples/ui/_field.html.haml +9 -0
  125. data/app/views/cafe_car/examples/ui/_grid.html.haml +11 -0
  126. data/app/views/cafe_car/examples/ui/_group.html.haml +21 -0
  127. data/app/views/cafe_car/examples/ui/_info_circle.html.haml +1 -0
  128. data/app/views/cafe_car/examples/ui/_menu.html.haml +5 -0
  129. data/app/views/cafe_car/examples/ui/_modal.html.haml +4 -0
  130. data/app/views/cafe_car/examples/ui/_navigation.html.haml +4 -0
  131. data/app/views/cafe_car/examples/ui/_page.html.haml +4 -0
  132. data/app/views/cafe_car/examples/ui/_table.html.haml +13 -0
  133. data/app/views/cafe_car/layouts/mailer.html.haml +8 -0
  134. data/app/views/cafe_car/layouts/mailer.text.erb +1 -0
  135. data/app/views/layouts/action_text/contents/_content.html.erb +3 -0
  136. data/app/views/layouts/application.html.haml +4 -0
  137. data/app/views/layouts/mailer.html.erb +13 -0
  138. data/app/views/layouts/mailer.text.erb +1 -0
  139. data/app/views/notes/_fields.html.haml +1 -0
  140. data/app/views/passwords_mailer/reset.html.haml +5 -0
  141. data/app/views/passwords_mailer/reset.text.erb +4 -0
  142. data/app/views/ui/_card.html.haml +8 -0
  143. data/app/views/ui/_field.html.haml +1 -0
  144. data/app/views/ui/_modal_close.html.haml +1 -0
  145. data/app/views/ui/_page.html.haml +7 -0
  146. data/config/brakeman.ignore +77 -0
  147. data/config/importmap.rb +12 -0
  148. data/config/locales/en.yml +63 -0
  149. data/config/routes.rb +9 -0
  150. data/db/migrate/20251005220017_create_slugs.rb +13 -0
  151. data/lib/cafe_car/active_record.rb +21 -0
  152. data/lib/cafe_car/application_responder.rb +17 -0
  153. data/lib/cafe_car/attributes.rb +23 -0
  154. data/lib/cafe_car/auto_resolver.rb +49 -0
  155. data/lib/cafe_car/caching.rb +20 -0
  156. data/lib/cafe_car/component.rb +155 -0
  157. data/lib/cafe_car/context.rb +17 -0
  158. data/lib/cafe_car/controller/filtering.rb +30 -0
  159. data/lib/cafe_car/controller.rb +218 -0
  160. data/lib/cafe_car/core_ext/array.rb +24 -0
  161. data/lib/cafe_car/core_ext/hash.rb +15 -0
  162. data/lib/cafe_car/core_ext/module.rb +15 -0
  163. data/lib/cafe_car/core_ext.rb +5 -0
  164. data/lib/cafe_car/current.rb +9 -0
  165. data/lib/cafe_car/engine.rb +107 -0
  166. data/lib/cafe_car/field_builder.rb +44 -0
  167. data/lib/cafe_car/field_info.rb +144 -0
  168. data/lib/cafe_car/fields.rb +21 -0
  169. data/lib/cafe_car/filter/field_builder.rb +4 -0
  170. data/lib/cafe_car/filter/field_info.rb +22 -0
  171. data/lib/cafe_car/filter/form_builder.rb +21 -0
  172. data/lib/cafe_car/filter.rb +5 -0
  173. data/lib/cafe_car/filter_builder.rb +20 -0
  174. data/lib/cafe_car/form_builder.rb +105 -0
  175. data/lib/cafe_car/generators.rb +30 -0
  176. data/lib/cafe_car/helpers.rb +178 -0
  177. data/lib/cafe_car/href_builder.rb +97 -0
  178. data/lib/cafe_car/informable.rb +9 -0
  179. data/lib/cafe_car/input_builder.rb +25 -0
  180. data/lib/cafe_car/inputs/association_builder.rb +6 -0
  181. data/lib/cafe_car/inputs/base_input.rb +19 -0
  182. data/lib/cafe_car/inputs/belongs_to_builder.rb +6 -0
  183. data/lib/cafe_car/inputs/password_input.rb +7 -0
  184. data/lib/cafe_car/inputs/string_input.rb +7 -0
  185. data/lib/cafe_car/link_builder.rb +65 -0
  186. data/lib/cafe_car/model.rb +23 -0
  187. data/lib/cafe_car/model_info.rb +24 -0
  188. data/lib/cafe_car/name_patch.rb +17 -0
  189. data/lib/cafe_car/navigation.rb +76 -0
  190. data/lib/cafe_car/option_helpers.rb +53 -0
  191. data/lib/cafe_car/param_parser.rb +45 -0
  192. data/lib/cafe_car/pluralization.rb +15 -0
  193. data/lib/cafe_car/policy.rb +77 -0
  194. data/lib/cafe_car/proc_helpers.rb +13 -0
  195. data/lib/cafe_car/query_builder.rb +186 -0
  196. data/lib/cafe_car/queryable.rb +29 -0
  197. data/lib/cafe_car/resolver.rb +27 -0
  198. data/lib/cafe_car/routing.rb +17 -0
  199. data/lib/cafe_car/table/body_builder.rb +12 -0
  200. data/lib/cafe_car/table/builder.rb +52 -0
  201. data/lib/cafe_car/table/foot_builder.rb +14 -0
  202. data/lib/cafe_car/table/head_builder.rb +26 -0
  203. data/lib/cafe_car/table/label_builder.rb +48 -0
  204. data/lib/cafe_car/table/objects_builder.rb +8 -0
  205. data/lib/cafe_car/table/row_builder.rb +39 -0
  206. data/lib/cafe_car/table_builder.rb +13 -0
  207. data/lib/cafe_car/turbo_tag_builder.rb +7 -0
  208. data/lib/cafe_car/ui.rb +11 -0
  209. data/lib/cafe_car/version.rb +1 -1
  210. data/lib/cafe_car/visitors.rb +21 -0
  211. data/lib/cafe_car.rb +25 -168
  212. data/lib/generators/cafe_car/controller/USAGE +11 -0
  213. data/lib/generators/cafe_car/controller/controller_generator.rb +26 -0
  214. data/lib/generators/cafe_car/controller/templates/controller.rb.tt +5 -0
  215. data/lib/generators/cafe_car/install/USAGE +8 -0
  216. data/lib/generators/cafe_car/install/install_generator.rb +46 -0
  217. data/lib/generators/cafe_car/install/templates/application_policy.rb.tt +7 -0
  218. data/lib/generators/cafe_car/notes/USAGE +12 -0
  219. data/lib/generators/cafe_car/notes/notes_generator.rb +13 -0
  220. data/lib/generators/cafe_car/notes/templates/create_notes.rb.tt +12 -0
  221. data/lib/generators/cafe_car/notes/templates/notable.rb.tt +7 -0
  222. data/lib/generators/cafe_car/notes/templates/note.rb.tt +6 -0
  223. data/lib/generators/cafe_car/policy/USAGE +8 -0
  224. data/lib/generators/cafe_car/policy/policy_generator.rb +39 -0
  225. data/lib/generators/cafe_car/policy/templates/policy.rb.tt +20 -0
  226. data/lib/generators/cafe_car/resource/USAGE +13 -0
  227. data/lib/generators/cafe_car/resource/resource_generator.rb +32 -0
  228. data/lib/generators/cafe_car/sessions/USAGE +17 -0
  229. data/lib/generators/cafe_car/sessions/sessions_generator.rb +29 -0
  230. data/lib/generators/cafe_car/sessions/templates/create_sessions.rb.tt +12 -0
  231. data/lib/tasks/holdco_tasks.rake +532 -0
  232. data/lib/tasks/templates/tasks_header.md +37 -0
  233. metadata +444 -21
  234. data/app/views/cafe_car/application/_fields.html.erb +0 -7
  235. data/app/views/cafe_car/application/_filters.html.erb +0 -0
  236. data/app/views/cafe_car/application/_form.html.erb +0 -22
  237. data/lib/cafe_car/railtie.rb +0 -4
  238. /data/app/views/{cafe_car/application/_actions.html.erb → application/_aside.html.haml} +0 -0
  239. /data/app/views/{cafe_car/application/_aside.html.erb → application/_footer.html.haml} +0 -0
  240. /data/app/views/cafe_car/{application/_extra_fields.html.erb → examples/_index_actions.html.haml} +0 -0
@@ -0,0 +1,18 @@
1
+ module CafeCar
2
+ class CodePresenter < self[:Presenter]
3
+ def formatter = Rouge::Formatters::HTML.new
4
+ def source = object.to_s
5
+ def formatted = formatter.format(lexer.lex(source))
6
+
7
+ def options_lexer = options[:lang].try { Rouge::Lexer.find(_1) }
8
+ def guess_lexer = Rouge::Lexer.guess(source:)
9
+
10
+ def lexer
11
+ @lexer ||= options_lexer || guess_lexer
12
+ end
13
+
14
+ def to_html
15
+ ui.Code(formatted.html_safe, class: [ "highlight", lexer.tag ])
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,5 @@
1
+ module CafeCar
2
+ class CurrencyPresenter < CafeCar[:Presenter]
3
+ def to_html = @template.number_to_currency(object)
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ module CafeCar
2
+ module DateAndTime
3
+ class CompatibilityPresenter < DateTimePresenter
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module CafeCar
2
+ class DatePresenter < DateTimePresenter
3
+ def to_html = long
4
+ end
5
+ end
@@ -0,0 +1,11 @@
1
+ module CafeCar
2
+ class DateTimePresenter < Presenter
3
+ def distance = @template.time_ago_in_words(object)
4
+ def words = object.past? ? "#{distance} ago" : "in #{distance}"
5
+ def datetime = object.iso8601
6
+ def long = l(object, format: :long)
7
+
8
+ def string = words
9
+ def to_html = tag.time words, datetime:, "data-tip": long
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ module CafeCar
2
+ class EnumerablePresenter < self[:Presenter]
3
+ def count = options[:count] || object.count
4
+ def with_count(list) = t(:list_html, list:, count:)
5
+
6
+ def to_html
7
+ object.map { present(_1) }
8
+ .then { options[:count] == object.count ? _1 : [ _1, "..." ] }
9
+ .then { safe_join(_1, ", ") }
10
+ .then { with_count _1 }
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ module CafeCar
2
+ class FalseClassPresenter < CafeCar[:Presenter]
3
+ def to_html = t(object)
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ module CafeCar
2
+ class HashPresenter < self[:CodePresenter]
3
+ def lexer = Rouge::Lexers::JSON.new
4
+ def source = JSON.pretty_generate(object)
5
+ end
6
+ end
@@ -0,0 +1,13 @@
1
+ module CafeCar
2
+ class NilClassPresenter < self[:Presenter]
3
+ def nil? = true
4
+ def present? = false
5
+ def blank? = true
6
+
7
+ def then = self
8
+ def to_str = ""
9
+ def to_ary = []
10
+ def to_html = nil
11
+ def method_missing(...) = self
12
+ end
13
+ end
@@ -0,0 +1,157 @@
1
+ module CafeCar
2
+ # Acts 1:3
3
+ class Presenter
4
+ include Caching
5
+ include OptionHelpers
6
+
7
+ delegate *%w[l t capture concat link link_to partial? get_partial href_for render safe_join raw tag ui], to: :@template
8
+ delegate :context, :context?, to: :@template
9
+ delegate :model_name, to: :model
10
+
11
+ attr_reader :object, :options, :block
12
+ class_attribute :show_defaults, default: Hash.new { _1[_2] = {} }
13
+ option :blank, presence: false
14
+
15
+ def self.inherited(subclass)
16
+ super
17
+ subclass.show_defaults = show_defaults.deep_dup
18
+ end
19
+
20
+ def self.present(template, object, **options, &)
21
+ object = object.object if object.is_a?(Presenter)
22
+ find(options.fetch(:as) { object.class }).new(template, object, **options, &)
23
+ end
24
+
25
+ def self.find(klass)
26
+ candidates(klass).filter_map { CafeCar[_1] }.first or raise "Could not find presenter"
27
+ end
28
+
29
+ def self.candidates(klass)
30
+ names(klass).map { "#{_1}Presenter" }
31
+ end
32
+
33
+ def self.names(klass)
34
+ return [ klass.to_s.classify ] if klass.is_a?(Symbol)
35
+ klass.ancestors.lazy.map(&:name).compact
36
+ end
37
+
38
+ def self.show(method, block = nil, **)
39
+ show_defaults[method].merge!({ block: }.compact, **)
40
+ end
41
+
42
+ def initialize(template, object, **options, &block)
43
+ @template = template
44
+ @object = object
45
+ @options = options
46
+ @block = options.delete(:block) { block }
47
+ @shown_attributes = {}
48
+ assign_options!
49
+ end
50
+
51
+ def to_model = @object
52
+ def model = @object.is_a?(Class) ? @object : @object.class
53
+ def html_safe? = true
54
+ def to_s = to_html.to_s
55
+ def present(...) = @template.present(...)
56
+ def partial = @object.try(:to_partial_path)
57
+ def has_partial? = partial&.then { partial? _1 }
58
+
59
+ derive :policy, -> { @template.policy(@object) }
60
+ derive :captured, -> { block ? capture(object, &block) : object.to_s }
61
+
62
+ def to_html
63
+ return render(object:, partial:) if has_partial?
64
+ return blank if captured.blank? && blank
65
+ preview
66
+ end
67
+
68
+ def title(*, **, &) = show(policy.title_attribute, *, blank: show(:id), **, &)
69
+ def logo(*, **, &) = show(policy.logo_attribute, *, **, &)
70
+
71
+ def timestamps(**, &)
72
+ attributes(*model.info.fields.timestamps.names, **, &)
73
+ end
74
+
75
+ def attributes(*methods, except: nil, **options, &block)
76
+ methods = policy.displayable_attributes if methods.empty?
77
+ methods = methods.flatten.compact
78
+ methods -= [ *except ]
79
+ capture do
80
+ methods.map do |method|
81
+ attribute(method, **options, &block)
82
+ end.each { concat(_1) }
83
+ end
84
+ end
85
+
86
+ def links = link(object)
87
+
88
+ # The object's canonical path, or nil when it isn't independently routable
89
+ # (e.g. a nested resource like a line item).
90
+ def href
91
+ href_for(object)
92
+ rescue NoMethodError, ActionController::UrlGenerationError
93
+ nil
94
+ end
95
+
96
+ def preview(**, &)
97
+ ui.Row :space, href: do
98
+ concat logo(size: :icon)
99
+ concat title
100
+ end
101
+ end
102
+
103
+ def attribute(method, **, &)
104
+ content = show(method, &)
105
+ return "" if content.blank?
106
+
107
+ ui.Field do |f|
108
+ concat f.Label(safe_join([ human(method), *info_circle(method) ], " "), tag: :strong)
109
+ concat f.Content(content)
110
+ end
111
+ end
112
+
113
+ def info(method) = model.info.field(method)
114
+ def human(method, **) = model.human_attribute_name(method, **)
115
+
116
+ def value(method)
117
+ model.inspection_filter.filter_param(method, object.try(method))
118
+ end
119
+
120
+ def show(method, **options, &)
121
+ return if method.nil?
122
+ @shown_attributes[method] = true
123
+ present(value(method), **show_defaults[method], **options, &)
124
+ end
125
+
126
+ def remaining_attributes(count = nil, **options, &block)
127
+ attrs = policy.displayable_attributes - @shown_attributes.keys
128
+ attrs = attrs.take(count) if count
129
+ attributes(*attrs, **options, &block)
130
+ end
131
+
132
+ def associations(*names, **options, &block)
133
+ names = policy.displayable_associations if names.blank?
134
+ attributes(*names, **options, &block)
135
+ end
136
+
137
+ def info_circle(method, *args, **opts, &block)
138
+ tip = info(method).hint
139
+ return unless tip
140
+ ui.InfoCircle(*args, tip:, **opts, &block)
141
+ end
142
+
143
+ def controls(**options, &block)
144
+ render("controls", object:, options:, &block)
145
+ end
146
+
147
+ def i18n_vars(names) = names.merge(*names.map { { _1.to_s.downcase.to_sym => _2.downcase } })
148
+
149
+ def i18n(action, scope: nil, **)
150
+ vars = i18n_vars Action: t(action, default: action.to_s.humanize),
151
+ Model: model_name.human,
152
+ Models: model_name.human(count: 2),
153
+ Object: link(@object).to_s.html_safe
154
+ @template.translate(action, scope:, default: :default, deep_interpolation: true, **vars, **)
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,16 @@
1
+ module CafeCar
2
+ class RangePresenter < CafeCar[:Presenter]
3
+ def to_html
4
+ min, max = object.begin, object.end
5
+ if min.blank? && max.blank?
6
+ ""
7
+ elsif max.blank? || max == Float::INFINITY
8
+ "#{min}+"
9
+ elsif min == max
10
+ min
11
+ else
12
+ "#{min} – #{max}"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,5 @@
1
+ module CafeCar
2
+ class RecordPresenter < CafeCar[:Presenter]
3
+ show :id, -> { "##{_1}" }
4
+ end
5
+ end
@@ -0,0 +1,20 @@
1
+ module CafeCar
2
+ class StringPresenter < self[:Presenter]
3
+ def title = to_s
4
+
5
+ def to_s
6
+ length = @options[:truncate]
7
+ txt = object
8
+ txt = @template.truncate(txt, length:) if length
9
+
10
+ case object
11
+ when %r{^https?://.+\.(png|jpe?g|svg)$}
12
+ @template.image_tag object, style: "width: 1em"
13
+ when %r{^https?://}
14
+ link_to(txt, object, target: "_blank", rel: "noopener")
15
+ else
16
+ txt
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,5 @@
1
+ module CafeCar
2
+ class SymbolPresenter < CafeCar[:Presenter]
3
+ def to_html = object.to_s.humanize
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module CafeCar
2
+ class TrueClassPresenter < CafeCar[:Presenter]
3
+ def to_html = t(object)
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ module CafeCar
2
+ module UI
3
+ component :Button do
4
+ flag :current
5
+ flag :primary
6
+ flag :danger
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,18 @@
1
+ module CafeCar
2
+ module UI
3
+ component :Card do
4
+ flag :slim
5
+ option :title
6
+ option :subtitle
7
+ option :image
8
+ option :actions
9
+ option :tabs
10
+
11
+ component :Head, :Aside, :Body, :Foot
12
+ component :Title, tag: :h2
13
+ component :Subtitle, tag: :h3
14
+ component :Image, tag: :img
15
+ component :Actions
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,11 @@
1
+ module CafeCar
2
+ module UI
3
+ component :Field do
4
+ component :Label
5
+ component :Hint
6
+ component :Input
7
+ component :Content
8
+ component :Error
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,30 @@
1
+ module CafeCar
2
+ module UI
3
+ component :Grid do
4
+ option :columns
5
+ option :template
6
+ option :style
7
+
8
+ def attributes
9
+ super.merge(style:)
10
+ end
11
+
12
+ def style
13
+ style = [ *@style ]
14
+ style << "grid-template: #{template}" if template?
15
+ style << "grid-template-columns: #{columns}" if columns?
16
+ style.compact_blank.join("; ").presence
17
+ end
18
+
19
+ def columns
20
+ case @columns
21
+ in Numeric
22
+ "repeat(#{@columns}, 1fr)"
23
+ in [ a, b ]
24
+ "repeat(auto-fill, minmax(#{a}, #{b}))"
25
+ else @columns
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,7 @@
1
+ module CafeCar
2
+ module UI
3
+ component :Layout do
4
+ component :Menu, tag: :input, type: :checkbox
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,14 @@
1
+ module CafeCar
2
+ module UI
3
+ component :Page do
4
+ flag :slim
5
+ option :title
6
+ option :actions
7
+ option :tabs
8
+
9
+ component :Head, :Aside, :Body, :Foot
10
+ component :Title, tag: :h2
11
+ component :Actions
12
+ end
13
+ end
14
+ end
@@ -0,0 +1 @@
1
+ = yield :actions
@@ -0,0 +1,2 @@
1
+ - flash.each do |k, v|
2
+ = Alert(k.to_sym, v)
@@ -0,0 +1,7 @@
1
+ = Layout do |l|
2
+ = l.Sidebar do
3
+ - title = t(:title, scope: [:application], default: Rails.application.name.titleize)
4
+ %h2= link_to title, root_path
5
+ = render "navigation"
6
+ = l.Main { yield }
7
+ = l.Footer { render("footer") }
@@ -0,0 +1,12 @@
1
+ - index = options.delete(:index) { false }
2
+ - show = options.delete(:show) { false }
3
+ - edit = options.delete(:edit) { true }
4
+ - destroy = options.delete(:destroy) { true }
5
+ - links = link(object)
6
+
7
+ -# TODO: Handle resources with missing routes
8
+ = Controls tag: :span, **options do |c|
9
+ = c.Link links.index if index
10
+ = c.Link links.show if show
11
+ = c.Link links.edit if edit
12
+ = c.Link links.destroy if destroy
@@ -0,0 +1,18 @@
1
+ - console if Rails.env.development?
2
+
3
+ = Card title: "Debug" do |card|
4
+ = card.Section do
5
+ = Field label: "Params" do
6
+ = p parsed_params
7
+
8
+ = Field label: "Query" do
9
+ = p scope.to_sql, as: :code, lang: :sql
10
+
11
+ - if session = current_session
12
+ = Field label: "Session" do
13
+ = p session.as_json
14
+
15
+ = Field label: "Cookies" do
16
+ = p cookies.signed.as_json.to_s, as: :code
17
+
18
+ = yield :debug
@@ -0,0 +1 @@
1
+ No #{model_name.human(count: 0).downcase} yet.
@@ -0,0 +1,4 @@
1
+ - if errors = f.object.errors.full_messages.presence
2
+ = Error do
3
+ = to_sentence(f.object.errors.messages[:base]).presence || "Correct the errors above."
4
+ %p= to_sentence(errors)
@@ -0,0 +1,5 @@
1
+ = field.wrapper do
2
+ = field.label
3
+ = field.error
4
+ = field.input
5
+ = field.hint
@@ -0,0 +1 @@
1
+ = f.remaining_fields
@@ -0,0 +1,8 @@
1
+ = Card :sticky do |card|
2
+ = filter_form_for objects do |f|
3
+ = card.Section do
4
+ - f.remaining_attributes.each do |attr|
5
+ = f.field attr
6
+
7
+ = card.Foot do
8
+ = f.submit "Filter", commit: false
@@ -0,0 +1,6 @@
1
+ = form_for object, url: href_for(object) do |f|
2
+ = card.Section do
3
+ = render "fields", f:, card:
4
+ = card.Foot do
5
+ = render "submit", f:, card:
6
+ = render "errors", f:, card:
@@ -0,0 +1,3 @@
1
+ = Grid do |grid|
2
+ - present(objects).each do |object|
3
+ = render "grid_item", object:, grid:
@@ -0,0 +1 @@
1
+ = Card title: object.title, image: object.logo(href: object), actions: object.controls
@@ -0,0 +1,17 @@
1
+ %title= yield :title
2
+
3
+ %meta{name: "viewport", content: "width=device-width,initial-scale=1"}
4
+ %meta{name: "apple-mobile-web-app-capable", content:"yes"}
5
+ %meta{name: "mobile-web-app-capable", content:"yes"}
6
+ %meta{name: "turbo-prefetch", content: "true"}
7
+ %meta{name: "turbo-refresh-method", content: "morph"}
8
+ %meta{name: "turbo-refresh-scroll", content: "preserve"}
9
+ %meta{name: "view-transition", content: "same-origin"}
10
+ %meta{name: "turbo-cache-control", content: "no-preview"}
11
+ = csrf_meta_tags
12
+ = csp_meta_tag
13
+
14
+ = yield :head
15
+
16
+ = stylesheet_link_tag "application", "data-turbo-track": "dynamic"
17
+ = javascript_importmap_tags
@@ -0,0 +1,8 @@
1
+ = render(view).presence or render("empty")
2
+
3
+ .center
4
+ = page_entries_info objects
5
+ - if filtered?
6
+ matching your filters. #{link(model).index "View all"}
7
+
8
+ = paginate objects
@@ -0,0 +1,7 @@
1
+ = Group do
2
+ = Button href: view_url(:grid), tip: "Grid View" do
3
+ = icon(:view_grid)
4
+ = Button href: view_url(:table), tip: "Table View" do
5
+ = icon(:table_rows)
6
+
7
+ = link(model.build).new(class: ui.Button(:primary).class_name, params: dot_params) { icon(:plus) + _1}
@@ -0,0 +1,9 @@
1
+ = Navigation tag: :nav do |nav|
2
+ = render "navigation_links", nav:
3
+
4
+ - if try(:authenticated?)
5
+ = nav.Link(:bottom, href: try(:session_path)) { p(current_session) }
6
+ - elsif href = try(:new_session_path)
7
+ = nav.Link(:bottom, href:) do
8
+ = Icon t(:new_session, scope: "navigation.icon").to_sym
9
+ Sign In
@@ -0,0 +1,5 @@
1
+ - navigation.groups.each do |name, routes|
2
+ - if name.present?
3
+ %h4= name.join(" ").titleize
4
+ - routes.each do |r|
5
+ = r.link
@@ -0,0 +1,10 @@
1
+ - return unless object.respond_to? :notes
2
+ - href_for(:notes) rescue return
3
+
4
+ - ui(object.notes).each do |note|
5
+ = card.Section :block do
6
+ %small.shy.right= note.controls
7
+ = note.show :body
8
+ — #{note.show :author}
9
+
10
+ = render "form", object: object.notes.new, card:
@@ -0,0 +1,7 @@
1
+ = turbo_stream.append_all("body") do
2
+ = Modal :fixed, id: dom_id(object, :modal), class: "popup", data: {turbo_permanent: true} do |m|
3
+ = yield
4
+
5
+ -# = turbo_stream.after_all ui.Page.Body.selector do
6
+ -# = ui.Page.Aside class: "popup", id: dom_id(object, :popup), data: {turbo_permanent: true} do
7
+ -# = yield
@@ -0,0 +1,9 @@
1
+ :ruby
2
+ p = ui(object)
3
+
4
+ = Card do |card|
5
+ = card.Section do
6
+ = p.remaining_attributes
7
+
8
+ = card.Section do
9
+ = p.associations
@@ -0,0 +1 @@
1
+ = f.submit data: { turbo_submits_with: "Saving..." }
@@ -0,0 +1,6 @@
1
+ = table_for objects do |t|
2
+ = t.logo
3
+ = t.title
4
+ = t.remaining
5
+ = t.timestamps
6
+ = t.controls
@@ -0,0 +1,2 @@
1
+ = turbo_stream.refresh request_id: nil
2
+ = turbo_stream.remove dom_id(model.new, :modal)
@@ -0,0 +1 @@
1
+ = turbo_stream.refresh request_id: nil
@@ -0,0 +1,18 @@
1
+ - title = breadcrumbs(link(object).index, link(object), title("Edit #{model_name.human}"))
2
+
3
+ = Page :slim, title: do |page|
4
+ = page.Body do
5
+ = render "alerts"
6
+ = ui.Card do |card|
7
+ = render 'form', card:
8
+ = render "debug" if debug?
9
+
10
+ = page.Aside do
11
+ = Card :sticky do |card|
12
+ = card.Section ui(object).controls(edit: false)
13
+ - if object.persisted?
14
+ = card.Section do
15
+ = ui(object).attributes :updated_at, :created_at, try: true
16
+
17
+ = Card title: "Actions" do
18
+ = render('actions')
@@ -0,0 +1,7 @@
1
+ - if action.edit?
2
+ = render "popup" do
3
+ = Card title: "Edit #{model_name.human}", actions: Close { icon(:xmark_circle_solid) } do |card|
4
+ = render "form", card:
5
+ - else
6
+ = turbo_stream.replace(dom_id(object, :edit)) do
7
+ = render "form", card: ui.Card
@@ -0,0 +1,15 @@
1
+ - title = title model_name.human(count: :all)
2
+ - actions = render "index_actions"
3
+ - body = render("index")
4
+
5
+ = Page title:, actions: do |page|
6
+ -# = page.Aside do
7
+ -# = render "filters"
8
+
9
+ = page.Body do
10
+ = render "alerts"
11
+ = body
12
+ = render "debug" if debug?
13
+
14
+ = page.Foot do
15
+ = yield :foot