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,105 @@
1
+ .Page {
2
+ container: Page;
3
+ display: flex;
4
+ flex-flow: wrap;
5
+ justify-content: start;
6
+ gap: var(--gap);
7
+ background: var(--background);
8
+ border-radius: var(--radius);
9
+ min-height: 100%;
10
+ align-content: start;
11
+ }
12
+
13
+ .Page-center {
14
+ justify-content: start;
15
+ padding: var(--gap);
16
+ }
17
+
18
+ .Page_Head {
19
+ display: flex;
20
+ align-items: center;
21
+ gap: var(--gap);
22
+ padding-left: var(--gap);
23
+ width: 100%;
24
+ top: var(--gap);
25
+ left: var(--gap);
26
+ }
27
+
28
+ .Page_Foot {
29
+ width: 100%;
30
+ }
31
+
32
+ .Page_Title {
33
+ margin: 0;
34
+ }
35
+
36
+ .Page_Actions {
37
+ display: flex;
38
+ gap: var(--gap);
39
+ margin-left: auto;
40
+ }
41
+
42
+ @keyframes Page_Aside-popup {
43
+ from {
44
+ margin-right: -400px;
45
+ opacity: 0;
46
+ }
47
+
48
+ to {
49
+ margin-right: 0px;
50
+ opacity: 1;
51
+ }
52
+ }
53
+
54
+ .Page_Aside {
55
+ min-width: 300px;
56
+ max-width: 720px;
57
+ display: flex;
58
+ flex-flow: column;
59
+ gap: var(--gap);
60
+
61
+ &.popup {
62
+ animation: Page_Aside-popup var(--duration);
63
+ z-index: 2;
64
+ }
65
+
66
+ &.remove {
67
+ opacity: 1;
68
+ margin-right: -400px;
69
+ pointer-events: none;
70
+ }
71
+ }
72
+
73
+ .Page_Body {
74
+ flex: 1 1;
75
+ width: 0;
76
+ display: flex;
77
+ flex-flow: column;
78
+ gap: var(--gap);
79
+ /* overflow: auto; */
80
+ }
81
+
82
+ .Page-slim .Page_Body,
83
+ .Page_Body-slim {
84
+ max-width: 720px;
85
+ }
86
+
87
+ .Page-center .Page_Body {
88
+ margin-left: auto;
89
+ margin-right: auto;
90
+ }
91
+
92
+ @media (width < 720px) {
93
+ .Page {
94
+ flex-flow: column;
95
+ justify-items: stretch;
96
+ }
97
+
98
+ .Page_Head,
99
+ .Page_Body,
100
+ .Page_Aside,
101
+ .Page_Foot {
102
+ justify-self: stretch;
103
+ width: auto;
104
+ }
105
+ }
@@ -0,0 +1,9 @@
1
+ .Row {
2
+ display: inline-flex;
3
+ justify-items: stretch;
4
+ align-items: center;
5
+ }
6
+
7
+ .Row-space {
8
+ gap: .3em;
9
+ }
@@ -0,0 +1,101 @@
1
+ .Table {
2
+ display: grid;
3
+ grid-template-columns: repeat(20, auto);
4
+ align-items: stretch;
5
+ box-shadow: var(--box-shadow);
6
+ border-radius: var(--radius);
7
+ background: var(--card);
8
+ min-width: min-content;
9
+ }
10
+
11
+ .Table_Head,
12
+ .Table_Body,
13
+ .Table_Foot,
14
+ .Table_Row {
15
+ display: grid;
16
+ grid-template-columns: subgrid;
17
+ grid-column: 1/-1;
18
+ }
19
+
20
+ .Table_Head {
21
+ z-index: 2;
22
+ white-space: nowrap;
23
+ font-weight: bold;
24
+ position: relative;
25
+ background: var(--card);
26
+ border-bottom: solid 2px var(--border);
27
+ border-top-left-radius: inherit;
28
+ border-top-right-radius: inherit;
29
+
30
+ .Table_Cell {
31
+ &:first-child {
32
+ border-top-left-radius: inherit;
33
+ }
34
+
35
+ &:last-child {
36
+ border-top-right-radius: inherit;
37
+ }
38
+
39
+ &:not(:empty) + &:not(:empty)::before {
40
+ display: block;
41
+ cursor: col-resize;
42
+ content: "";
43
+ inset: var(--gap) auto var(--gap) 0;
44
+ position: absolute;
45
+ border-left: 1px solid var(--border);
46
+ }
47
+ }
48
+
49
+ a {
50
+ text-decoration: none;
51
+ color: inherit;
52
+ }
53
+ }
54
+
55
+ .Table_Head-sticky {
56
+ position: sticky;
57
+ top: 0;
58
+ }
59
+
60
+ .Table_Row {
61
+ align-items: center;
62
+
63
+ &:not(:hover) .Table_Cell-shy {
64
+ visibility: hidden;
65
+ }
66
+
67
+ & + & {
68
+ border-top: 1px solid var(--background);
69
+ }
70
+ }
71
+
72
+ .Table_Cell {
73
+ position: relative;
74
+ padding: 0.75em 1em;
75
+ align-items: center;
76
+ white-space: nowrap;
77
+ overflow: hidden;
78
+ text-overflow: ellipsis;
79
+ gap: 2px;
80
+
81
+ > code {
82
+ white-space: nowrap;
83
+ }
84
+
85
+ &:has(img) {
86
+ padding-block: 0;
87
+ }
88
+
89
+ > img,
90
+ > a > img {
91
+ height: 2em;
92
+ /* margin: 4px; */
93
+ display: inline-block;
94
+ border-radius: 99px;
95
+ max-width: none;
96
+ }
97
+
98
+ &-controls {
99
+ text-align: right;
100
+ }
101
+ }
@@ -0,0 +1,24 @@
1
+ @import url("Alert.css");
2
+ @import url("Article.css");
3
+ @import url("Button.css");
4
+ @import url("Close.css");
5
+ @import url("Card.css");
6
+ @import url("Chat.css");
7
+ @import url("Code.css");
8
+ @import url("Controls.css");
9
+ @import url("Error.css");
10
+ @import url("Field.css");
11
+ @import url("Grid.css");
12
+ @import url("Icon.css");
13
+ @import url("Input.css");
14
+ @import url("Image.css");
15
+ @import url("InfoCircle.css");
16
+ @import url("Layout.css");
17
+ @import url("Menu.css");
18
+ @import url("Modal.css");
19
+ @import url("Navigation.css");
20
+ @import url("Page.css");
21
+ @import url("Row.css");
22
+ @import url("Table.css");
23
+
24
+ @import url("Group.css");
@@ -0,0 +1,9 @@
1
+ class ApplicationController < ActionController::Base
2
+ protect_from_forgery with: :exception
3
+ end
4
+
5
+ module CafeCar
6
+ class ApplicationController < ::ApplicationController
7
+ include Controller
8
+ end
9
+ end
@@ -0,0 +1,22 @@
1
+ require "rouge"
2
+ require "faker"
3
+
4
+ module CafeCar
5
+ class ExamplesController < const(:ApplicationController)
6
+ include Controller
7
+ helper CafeCar::Helpers
8
+
9
+ before_action :skip_policy_scope, only: :index
10
+ before_action :skip_authorization, only: :index
11
+
12
+ def index
13
+ @examples = view_context.template_glob("cafe_car/examples/ui/*")
14
+ .map { _1.name.sub(/\..+$/, "") }
15
+ .to_h { [ _1.camelize, "cafe_car/examples/ui/#{_1}" ] }
16
+ end
17
+
18
+ private
19
+
20
+ def model_name = @model_name ||= ActiveModel::Name.new(:components)
21
+ end
22
+ end
@@ -0,0 +1,30 @@
1
+ module CafeCar
2
+ class SessionsController < const(:ApplicationController)
3
+ include Controller
4
+
5
+ cafe_car model: CafeCar[:Session]
6
+ rate_limit to: 10, within: 3.minutes, only: :create # , with: -> { redirect_to new_session_path, alert: "Try again later." }
7
+ before_action :skip_policy_scope, except: :index
8
+
9
+ after_create :persist_session
10
+ after_destroy :terminate_session
11
+
12
+ def create
13
+ run_callbacks(:create) { object.save! }
14
+ respond_with object, location: after_authentication_url
15
+ end
16
+
17
+ def destroy
18
+ run_callbacks(:destroy) { object.destroy! }
19
+ respond_with object, location: main_app.root_path
20
+ end
21
+
22
+ private
23
+
24
+ def find_object
25
+ self.object = current_session
26
+ end
27
+
28
+ def build_object = find_object
29
+ end
30
+ end
@@ -0,0 +1,61 @@
1
+ module CafeCar
2
+ module Authentication
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ helper_method :authenticated?, :current_user, :current_session
7
+ end
8
+
9
+ private
10
+
11
+ # Sessions/login are opt-in: a host enables them by running the
12
+ # `cafe_car:sessions` generator and exposing the session routes. When that
13
+ # infrastructure is absent we can't redirect to a login page, so callers
14
+ # fall back to 403 Forbidden instead of 500ing on a missing route.
15
+ def sessions_available?
16
+ respond_to?(:new_session_path) && CafeCar.sessions_available?
17
+ rescue StandardError
18
+ false
19
+ end
20
+
21
+ def authenticated?
22
+ current_user
23
+ end
24
+
25
+ def current_user
26
+ current_session.user
27
+ end
28
+
29
+ def current_session
30
+ CafeCar[:Current].session ||= find_session_by_cookie || build_session
31
+ end
32
+
33
+ def build_session
34
+ CafeCar[:Session].new user_agent: request.user_agent,
35
+ ip_address: request.remote_ip
36
+ end
37
+
38
+ def terminate_session
39
+ CafeCar[:Current].session.destroy!
40
+ CafeCar[:Current].session = nil
41
+ cookies.delete(:session_id)
42
+ end
43
+
44
+ def persist_session
45
+ cookies.signed.permanent[:session_id] = { value: current_session.id, httponly: true, same_site: :lax }
46
+ end
47
+
48
+ def find_session_by_cookie
49
+ cookies.signed[:session_id].try { Session.find(_1) }
50
+ end
51
+
52
+ def request_authentication
53
+ session[:return_to_after_authenticating] = request.url
54
+ redirect_to new_session_path, warning: t(:auth_required, new_session: t(:new_session))
55
+ end
56
+
57
+ def after_authentication_url
58
+ session.delete(:return_to_after_authenticating) || root_url
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,5 @@
1
+ import "@hotwired/turbo-rails"
2
+ import "cafe_car"
3
+
4
+ import "trix"
5
+ import "@rails/actiontext"
@@ -0,0 +1,174 @@
1
+ import "@hotwired/turbo-rails"
2
+
3
+ Object.assign(Turbo.StreamActions, {
4
+ // <turbo-stream action="navigate" target="Hello, world"></turbo-stream>
5
+ navigate() {
6
+ history.pushState({ action: "navigate" }, "", this.getAttribute("target"))
7
+ }
8
+ })
9
+
10
+ function last(arr) {
11
+ return arr[arr.length - 1];
12
+ }
13
+
14
+ addEventListener("mousedown", event => {
15
+ window.mouseDownTarget = event.target
16
+ })
17
+
18
+ addEventListener("mouseup", event => {
19
+ let isClose = event.target.closest(".Close") || event.target.matches(".Modal")
20
+ if (isClose && event.target === window.mouseDownTarget) {
21
+ event.preventDefault()
22
+ event.stopPropagation()
23
+ event.target.closest(".popup").classList.add("remove")
24
+ }
25
+ }, { capture: true })
26
+
27
+ addEventListener("keydown", event => {
28
+ switch (event.key) {
29
+ case "Escape":
30
+ let popup = event.target.closest(".popup") ||
31
+ last(document.querySelectorAll(".popup"))
32
+ if (popup) popup.classList.add("remove");
33
+ }
34
+ })
35
+
36
+ function animationEnd({ target }) {
37
+ if (target.matches(".remove")) target.remove()
38
+ else if (target.matches(".popup")) {
39
+ let input = target.querySelector("input:not([type=hidden]), textarea")
40
+ input?.focus()
41
+ input?.setSelectionRange(-1, -1)
42
+ }
43
+ }
44
+
45
+ addEventListener("animationend", animationEnd)
46
+ addEventListener("transitionend", animationEnd)
47
+
48
+ // NOTE: field-sizing property is used instead
49
+ // function adjustHeight(textarea) {
50
+ // textarea.style.height = "1px"
51
+ // textarea.style.height = (textarea.scrollHeight) + "px"
52
+ // }
53
+
54
+ // addEventListener("input", ({ target }) => {
55
+ // if (target.matches("textarea")) adjustHeight(target)
56
+ // })
57
+
58
+ export class Selection extends Array {
59
+ constructor(obj, ...rest) {
60
+ switch (typeof obj) {
61
+ case "string": {
62
+ super(...document.querySelectorAll(obj))
63
+ this.selector = obj
64
+ break
65
+ }
66
+ default: super(obj, ...rest); break
67
+ }
68
+ }
69
+
70
+ get clone() { return structuredClone(this) }
71
+
72
+ with(...objs) {
73
+ return this.clone.assign(...objs)
74
+ }
75
+
76
+ assign(...objs) {
77
+ for (let obj of objs) {
78
+ switch (typeof obj) {
79
+ case "function": obj(this)
80
+ default: Object.assign(this, obj)
81
+ }
82
+ }
83
+
84
+ return this
85
+ }
86
+
87
+ tap(fn) {
88
+ fn(this)
89
+ return this
90
+ }
91
+
92
+ static on(...x) { return this(window).on(...x) }
93
+ static off(...x) { return this(window).off(...x) }
94
+
95
+ select(v, ...rest) {
96
+ if (!v) return this
97
+
98
+ switch (typeof v) {
99
+ case "function": return super.filter(v).select(...rest)
100
+ case "string": return super.filter(e => e.matches(v)).select(...rest)
101
+ }
102
+ }
103
+
104
+ reject(v, ...rest) {
105
+ if (!v) return this
106
+ switch (typeof v) {
107
+ case "function": return super.filter(e => !v(e)).reject(...rest)
108
+ case "string": return super.filter(e => !e.matches(v)).reject(...rest)
109
+ }
110
+ }
111
+
112
+ each(...xs) { return this.forEach(...xs) }
113
+
114
+ forEach(...xs) {
115
+ super.forEach(...xs)
116
+ return this
117
+ }
118
+
119
+ add(name, ...names) {
120
+ if (!name) return this
121
+ if (name[0] == '.') name = name.slice(1)
122
+ return this.each(el => { el.classList.add(name) }).add(...names)
123
+ }
124
+
125
+ remove(name, ...names) {
126
+ if (!name) return this
127
+ if (name[0] == '.') name = name.slice(1)
128
+ return this.each(el => el.classList.remove(name)).remove(...names)
129
+ }
130
+
131
+ on(eventName, fn) {
132
+ return this.each(el => el.addEventListener(eventName, fn))
133
+ }
134
+
135
+ off(eventName, fn) {
136
+ return this.each(el => el.removeEventListener(eventName, fn))
137
+ }
138
+
139
+ set onhashchange(fn) { this.on("hashchange", fn) }
140
+ set onload(fn) { this.on("load", fn) }
141
+ set onclick(fn) { this.on("click", fn) }
142
+
143
+ get isLoaded() { return document.readyState == "complete" }
144
+
145
+ loaded(fn) {
146
+ return this.isLoaded
147
+ ? this.each(fn)
148
+ : this.on('load', fn)
149
+ }
150
+
151
+ onIntersection() {}
152
+
153
+ intersection(...options) {
154
+ return this.clone.tap(clone => {
155
+ clone.observer = new IntersectionObserver(clone.onIntersection.bind(clone), ...options)
156
+ })
157
+ }
158
+
159
+ observe(...options) {
160
+ return this.each(el => this.observer.observe(el, ...options))
161
+ }
162
+
163
+ unobserve(...options) {
164
+ this.each(el => this.observer.unobserve(el, ...options))
165
+ }
166
+ }
167
+
168
+ window.Selection = Selection
169
+ window.$ = function $(x, ...xs) {
170
+ switch (typeof x) {
171
+ case "function": return $(document).loaded(x)
172
+ default: return new Selection(x, ...xs)
173
+ }
174
+ }
@@ -0,0 +1,18 @@
1
+ module CafeCar
2
+ class Session < ApplicationRecord
3
+ belongs_to :user, class_name: CafeCar.user_class_name
4
+
5
+ attribute :email, :string
6
+ attribute :password, :password
7
+ attribute :login, :boolean, default: true
8
+
9
+ validates :email, :password, presence: true, if: :login?
10
+
11
+ before_validation :authenticate, if: :login?
12
+
13
+ def authenticate
14
+ self.user = CafeCar.user_class.authenticate_by(email:, password:) or
15
+ errors.add(:base, "Could not find user with given credentials")
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CafeCar::ApplicationPolicy
4
+ include CafeCar::Policy
5
+
6
+ attr_reader :user, :object
7
+
8
+ def initialize(user, object)
9
+ @user = user
10
+ @object = object
11
+ end
12
+
13
+ def index? = false
14
+ def show? = false
15
+ def create? = false
16
+ def new? = create?
17
+ def update? = false
18
+ def edit? = update?
19
+ def destroy? = false
20
+
21
+ def attributes
22
+ @attributes ||= Attributes.new(@user, @object, permitted_attributes)
23
+ end
24
+
25
+ class Scope
26
+ def initialize(user, scope)
27
+ @user = user
28
+ @scope = scope
29
+ end
30
+
31
+ def resolve
32
+ raise NoMethodError, "You must define #resolve in #{self.class}"
33
+ end
34
+
35
+ private
36
+
37
+ attr_reader :user, :scope
38
+ end
39
+
40
+ class Attributes < CafeCar::Attributes
41
+ end
42
+ end
@@ -0,0 +1,19 @@
1
+ module CafeCar
2
+ class SessionPolicy < ::ApplicationPolicy
3
+ def index? = admin?
4
+ def create? = true
5
+ def show? = true
6
+ def destroy? = admin? || mine?
7
+ def update? = false
8
+
9
+ def title_attribute = :user
10
+
11
+ def permitted_attributes
12
+ [ :email, :password ]
13
+ end
14
+
15
+ class Scope < Scope
16
+ def resolve = scope
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,7 @@
1
+ module CafeCar
2
+ module ActionText
3
+ class RichTextPresenter < CafeCar[:RecordPresenter]
4
+ def to_html = object
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ module CafeCar
2
+ module ActiveRecord
3
+ class BasePresenter < CafeCar[:RecordPresenter]
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,17 @@
1
+ module CafeCar
2
+ module ActiveRecord
3
+ class RelationPresenter < CafeCar[:Presenter]
4
+ def human(attribute, **options)
5
+ object.human_attribute_name(attribute, options)
6
+ end
7
+
8
+ def each = object.each { yield present(_1) }
9
+
10
+ def to_html
11
+ objects = object.to_a.uniq
12
+ objects = objects.sort_by(&:sort_key) if objects.first.respond_to?(:sort_key)
13
+ present objects, count: object.count, **@options
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ module CafeCar
2
+ module ActiveStorage
3
+ module Attached
4
+ class OnePresenter < AttachmentPresenter
5
+ def href = present(object.attachment).href
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,18 @@
1
+ module CafeCar
2
+ module ActiveStorage
3
+ class AttachmentPresenter < CafeCar[:Presenter]
4
+ option :size
5
+
6
+ # def url = object.representation(resize_to_limit: [300, 300])&.processed&.url
7
+ def url = object.try(:url)
8
+ def blank = options[:blank]
9
+ def logo = self
10
+
11
+ def image
12
+ @template.image_tag url, **options, class: ui.class(:image, size) if url
13
+ end
14
+
15
+ def preview = image || blank
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,5 @@
1
+ module CafeCar
2
+ class BasicObjectPresenter < CafeCar[:Presenter]
3
+ def to_html = captured.presence || options[:blank]
4
+ end
5
+ end