cafe_car 0.1.1 → 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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +155 -40
  3. data/Rakefile +9 -1
  4. data/app/assets/fonts/Lexend.css +7 -0
  5. data/app/assets/fonts/Lexend.ttf +0 -0
  6. data/app/assets/stylesheets/cafe_car/themes/defaults.css +58 -59
  7. data/app/assets/stylesheets/cafe_car/tooltips.css +20 -0
  8. data/app/assets/stylesheets/cafe_car/utility.css +1 -2
  9. data/app/assets/stylesheets/cafe_car.css +17 -6
  10. data/app/assets/stylesheets/ui/Alert.css +2 -1
  11. data/app/assets/stylesheets/ui/Button.css +6 -6
  12. data/app/assets/stylesheets/ui/Card.css +7 -3
  13. data/app/assets/stylesheets/ui/Chat.css +33 -0
  14. data/app/assets/stylesheets/ui/Close.css +11 -0
  15. data/app/assets/stylesheets/ui/Grid.css +2 -0
  16. data/app/assets/stylesheets/ui/Icon.css +3 -3
  17. data/app/assets/stylesheets/ui/Layout.css +20 -13
  18. data/app/assets/stylesheets/ui/Modal.css +5 -12
  19. data/app/assets/stylesheets/ui/Navigation.css +13 -5
  20. data/app/assets/stylesheets/ui/Page.css +42 -3
  21. data/app/assets/stylesheets/ui/Table.css +27 -56
  22. data/app/assets/stylesheets/ui/components.css +2 -0
  23. data/app/controllers/cafe_car/examples_controller.rb +1 -1
  24. data/app/controllers/cafe_car/sessions_controller.rb +30 -0
  25. data/app/controllers/concerns/cafe_car/authentication.rb +61 -0
  26. data/app/javascript/cafe_car.js +16 -11
  27. data/app/models/cafe_car/session.rb +18 -0
  28. data/app/policies/cafe_car/session_policy.rb +19 -0
  29. data/app/presenters/cafe_car/active_storage/attachment_presenter.rb +5 -4
  30. data/app/presenters/cafe_car/code_presenter.rb +18 -0
  31. data/app/presenters/cafe_car/date_presenter.rb +1 -0
  32. data/app/presenters/cafe_car/date_time_presenter.rb +2 -2
  33. data/app/presenters/cafe_car/enumerable_presenter.rb +1 -1
  34. data/app/presenters/cafe_car/hash_presenter.rb +3 -8
  35. data/app/presenters/cafe_car/presenter.rb +22 -12
  36. data/app/presenters/cafe_car/string_presenter.rb +2 -2
  37. data/app/ui/cafe_car/ui/button.rb +2 -1
  38. data/app/ui/cafe_car/ui/card.rb +18 -0
  39. data/app/ui/cafe_car/ui/grid.rb +30 -0
  40. data/app/ui/cafe_car/ui/layout.rb +7 -0
  41. data/app/ui/cafe_car/ui/page.rb +5 -1
  42. data/app/views/application/_body.html.haml +2 -1
  43. data/app/views/application/_controls.html.haml +1 -0
  44. data/app/views/application/_debug.html.haml +9 -2
  45. data/app/views/application/_errors.html.haml +4 -8
  46. data/app/views/application/_grid.html.haml +1 -1
  47. data/app/views/application/_grid_item.html.haml +1 -1
  48. data/app/views/application/_head.html.haml +1 -0
  49. data/app/views/application/_index.html.haml +6 -2
  50. data/app/views/application/_index_actions.html.haml +3 -3
  51. data/app/views/application/_navigation.html.haml +7 -0
  52. data/app/views/application/_navigation_links.html.haml +1 -1
  53. data/app/views/application/_notes.html.haml +1 -0
  54. data/app/views/application/_popup.html.haml +7 -0
  55. data/app/views/cafe_car/application/edit.html.haml +1 -1
  56. data/app/views/cafe_car/application/edit.turbo_stream.haml +3 -5
  57. data/app/views/cafe_car/application/index.html.haml +3 -0
  58. data/app/views/cafe_car/application/new.turbo_stream.haml +5 -6
  59. data/app/views/cafe_car/application/show.html.haml +2 -2
  60. data/app/views/cafe_car/examples/ui/_chat.html.haml +3 -0
  61. data/app/views/cafe_car/examples/ui/_info_circle.html.haml +1 -1
  62. data/app/views/cafe_car/examples/ui/_modal.html.haml +1 -1
  63. data/app/views/passwords_mailer/reset.html.haml +5 -0
  64. data/app/views/passwords_mailer/reset.text.erb +4 -0
  65. data/app/views/ui/_card.html.haml +6 -11
  66. data/app/views/ui/_field.html.haml +1 -7
  67. data/app/views/ui/_modal_close.html.haml +1 -2
  68. data/app/views/ui/_page.html.haml +6 -12
  69. data/config/brakeman.ignore +3 -3
  70. data/config/locales/en.yml +10 -2
  71. data/config/routes.rb +5 -1
  72. data/db/migrate/20251005220017_create_slugs.rb +2 -2
  73. data/lib/cafe_car/active_record.rb +1 -1
  74. data/lib/cafe_car/application_responder.rb +6 -0
  75. data/lib/cafe_car/attributes.rb +1 -1
  76. data/lib/cafe_car/auto_resolver.rb +1 -1
  77. data/lib/cafe_car/component.rb +102 -39
  78. data/lib/cafe_car/context.rb +5 -4
  79. data/lib/cafe_car/controller/filtering.rb +9 -1
  80. data/lib/cafe_car/controller.rb +52 -13
  81. data/lib/cafe_car/core_ext/array.rb +13 -0
  82. data/lib/cafe_car/core_ext/hash.rb +15 -0
  83. data/lib/cafe_car/core_ext/module.rb +15 -0
  84. data/lib/cafe_car/core_ext.rb +0 -2
  85. data/lib/cafe_car/current.rb +4 -1
  86. data/lib/cafe_car/engine.rb +9 -2
  87. data/lib/cafe_car/field_builder.rb +1 -1
  88. data/lib/cafe_car/field_info.rb +14 -12
  89. data/lib/cafe_car/fields.rb +7 -0
  90. data/lib/cafe_car/filter/field_info.rb +1 -1
  91. data/lib/cafe_car/filter/form_builder.rb +2 -2
  92. data/lib/cafe_car/filter_builder.rb +1 -1
  93. data/lib/cafe_car/form_builder.rb +1 -1
  94. data/lib/cafe_car/generators.rb +1 -1
  95. data/lib/cafe_car/helpers.rb +37 -10
  96. data/lib/cafe_car/href_builder.rb +35 -9
  97. data/lib/cafe_car/input_builder.rb +1 -1
  98. data/lib/cafe_car/link_builder.rb +14 -11
  99. data/lib/cafe_car/model.rb +2 -2
  100. data/lib/cafe_car/navigation.rb +10 -10
  101. data/lib/cafe_car/option_helpers.rb +11 -5
  102. data/lib/cafe_car/param_parser.rb +10 -6
  103. data/lib/cafe_car/policy.rb +2 -2
  104. data/lib/cafe_car/query_builder.rb +3 -3
  105. data/lib/cafe_car/resolver.rb +5 -1
  106. data/lib/cafe_car/routing.rb +1 -1
  107. data/lib/cafe_car/table/builder.rb +3 -2
  108. data/lib/cafe_car/table/head_builder.rb +2 -2
  109. data/lib/cafe_car/table/label_builder.rb +1 -1
  110. data/lib/cafe_car/table/row_builder.rb +5 -7
  111. data/lib/cafe_car/table_builder.rb +3 -3
  112. data/lib/cafe_car/ui.rb +2 -0
  113. data/lib/cafe_car/version.rb +1 -1
  114. data/lib/cafe_car/visitors.rb +2 -2
  115. data/lib/cafe_car.rb +25 -0
  116. data/lib/generators/cafe_car/controller/templates/controller.rb.tt +1 -1
  117. data/lib/generators/cafe_car/install/install_generator.rb +0 -1
  118. data/lib/generators/cafe_car/sessions/USAGE +17 -0
  119. data/lib/generators/cafe_car/sessions/sessions_generator.rb +29 -0
  120. data/lib/generators/cafe_car/sessions/templates/create_sessions.rb.tt +12 -0
  121. data/lib/tasks/holdco_tasks.rake +532 -0
  122. data/lib/tasks/templates/tasks_header.md +37 -0
  123. metadata +76 -48
  124. data/app/models/cafe_car/slug.rb +0 -3
  125. data/app/views/ui/_grid.html.haml +0 -17
  126. data/app/views/ui/_layout_menu.html.haml +0 -2
@@ -1,12 +1,13 @@
1
1
  .Button {
2
2
  --background: var(--button);
3
3
  --color: var(--color);
4
+ --darken: rgb(0 0 0 / 0.2);
5
+ --lighten: rgb(255 255 255 / 0.2);
4
6
  background: var(--background);
5
7
  color: var(--color);
6
8
  border-radius: var(--control-radius);
7
9
  padding: var(--half-gap);
8
- box-shadow: rgba(34, 32, 29, 0.05) 0px 1px 2px;
9
- /* border: 1px solid rgba(255,255,255, 0.3); */
10
+ box-shadow: inset var(--darken) 0 -1px 0 1px;
10
11
  border: 0;
11
12
  display: flex;
12
13
  align-items: center;
@@ -17,7 +18,7 @@
17
18
  .Button:where(:not(.Button-current)):hover {
18
19
  cursor: pointer;
19
20
  text-decoration: none;
20
- box-shadow: rgba(34, 32, 29, 0.1) 0px 1px 2px;
21
+ box-shadow: inset var(--darken) 0 -2px 0 1px;
21
22
  filter: brightness(1.05);
22
23
  z-index: 1;
23
24
  }
@@ -25,10 +26,9 @@
25
26
  .Button:active,
26
27
  .Button-current {
27
28
  text-decoration: none;
28
- box-shadow: rgba(34, 32, 29, 0.1) 0px 1px 2px;
29
29
  filter: brightness(0.95);
30
- /* border-style: inset; */
31
- box-shadow: inset rgba(34, 32, 29, 0.1) 0px 1px 2px;
30
+ border-bottom-width: 1px;
31
+ box-shadow: inset var(--darken) 0 1px;
32
32
  }
33
33
 
34
34
  .Button-primary {
@@ -8,8 +8,10 @@
8
8
  .Card_Image {
9
9
  display: block;
10
10
  aspect-ratio: 5/4;
11
- background-size: cover;
12
- background-position: center;
11
+ object-position: center;
12
+ object-fit: cover;
13
+ overflow: hidden;
14
+ width: 100%;
13
15
  border-top-left-radius: var(--radius);
14
16
  border-top-right-radius: var(--radius);
15
17
  /* &::before {
@@ -43,7 +45,9 @@
43
45
  }
44
46
 
45
47
  :is(.Card_Head, .Card_Section, .Card_Foot) {
46
- & + &, & + form > &, form > & + & {
48
+ & + &,
49
+ & + form > &,
50
+ form > & + & {
47
51
  border-top: 1px solid var(--border);
48
52
  }
49
53
  }
@@ -0,0 +1,33 @@
1
+ .Chat {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--gap);
5
+ padding: var(--gap);
6
+ }
7
+
8
+ .Chat_Message {
9
+ padding: var(--gap);
10
+ border-radius: var(--radius);
11
+ box-shadow: var(--box-shadow);
12
+ max-width: 80%;
13
+
14
+ }
15
+
16
+ .Chat_Message-local,
17
+ .Chat_Message-right {
18
+ align-self: end;
19
+ }
20
+
21
+ .Chat_Message-remote,
22
+ .Chat_Message-left {
23
+ align-self: start;
24
+ }
25
+
26
+ .Chat_Message-local {
27
+ background-color: var(--blue);
28
+ color: var(--white);
29
+ }
30
+
31
+ .Chat_Message-remote {
32
+ background-color: var(--gray);
33
+ }
@@ -0,0 +1,11 @@
1
+ .Close {
2
+ cursor: pointer;
3
+ line-height: 1;
4
+ opacity: 0.3;
5
+ }
6
+
7
+ .Close:hover {
8
+ opacity: 0.8;
9
+ text-decoration: none;
10
+ alignment-baseline: middle;
11
+ }
@@ -1,4 +1,6 @@
1
1
  .Grid {
2
+ container: Grid / inline-size;
2
3
  display: grid;
3
4
  gap: var(--gap);
5
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
4
6
  }
@@ -1,14 +1,14 @@
1
1
  @charset "utf-8";
2
2
 
3
3
  .Icon {
4
- vertical-align: bottom;
5
- display: flex;
4
+ vertical-align: text-bottom;
5
+ display: inline-flex;
6
6
  gap: var(--half-gap);
7
7
  font-style: inherit;
8
8
 
9
9
  &::before {
10
10
  display: block;
11
- content: '';
11
+ content: "";
12
12
  height: 1.5em;
13
13
  width: 1.5em;
14
14
  }
@@ -3,35 +3,44 @@
3
3
  }
4
4
 
5
5
  .Layout {
6
- display: flex;
7
- align-items: start;
6
+ /* display: flex; */
7
+ /* align-items: stretch; */
8
+ flex-grow: 1;
8
9
  }
9
10
 
10
11
  .Layout_Sidebar {
11
- --indent: .7rem;
12
+ --indent: 0.7rem;
12
13
 
13
14
  width: var(--sidebar-width);
14
- position: sticky;
15
+ /* position: sticky; */
16
+ position: fixed;
15
17
  inset: 0;
18
+
16
19
  overflow: auto;
20
+ max-height: 100dvh;
21
+ height: 100%;
17
22
 
18
23
  display: flex;
19
24
  flex-flow: column;
20
25
  flex-shrink: 0;
26
+ align-self: start;
21
27
 
22
28
  padding: var(--gap);
23
29
  white-space: nowrap;
24
- transition: margin var(--duration), width var(--duration);
30
+ transition:
31
+ margin var(--duration),
32
+ width var(--duration);
25
33
  }
26
34
 
27
35
  .Layout_Main {
28
36
  transition: transform var(--duration);
29
- /* margin-left: var(--sidebar-width); */
37
+ margin-left: var(--sidebar-width);
30
38
  display: flex;
31
39
  flex-flow: column;
32
40
  align-items: stretch;
33
41
  flex-grow: 1;
34
42
  padding: var(--gap);
43
+ position: relative;
35
44
  z-index: 1;
36
45
  }
37
46
 
@@ -43,7 +52,6 @@
43
52
  height: 40px;
44
53
  text-align: center;
45
54
  cursor: pointer;
46
- vertical-align: center;
47
55
  border-color: transparent;
48
56
  background: none;
49
57
 
@@ -62,7 +70,7 @@
62
70
  display: block;
63
71
  font-size: 30px;
64
72
  line-height: 1;
65
- margin-top: -3px;
73
+ margin-top: -1px;
66
74
  }
67
75
  }
68
76
 
@@ -72,16 +80,15 @@
72
80
  transform: translateX(var(--sidebar-width));
73
81
  }
74
82
 
83
+ .Layout_Sidebar {
84
+ margin-left: calc(-1 * var(--sidebar-width));
85
+ }
86
+
75
87
  body:has(.Layout_Menu:checked) {
76
88
  overflow-x: hidden;
77
89
  }
78
90
 
79
91
  body:not(:has(.Layout_Menu:checked)) {
80
- .Layout_Sidebar {
81
- margin-left: calc(-1 * var(--sidebar-width));
82
- /* width: 100vw; */
83
- }
84
-
85
92
  .Layout_Main {
86
93
  transform: none;
87
94
  }
@@ -1,5 +1,5 @@
1
1
  .Modal {
2
- z-index: 1;
2
+ z-index: 9;
3
3
  inset: 0;
4
4
  display: grid;
5
5
  grid-template-columns: fit-content(var(--modal-width));
@@ -10,18 +10,11 @@
10
10
  overflow: auto;
11
11
  backdrop-filter: blur(8px);
12
12
  padding: var(--gap);
13
- animation: fade-in 100ms;
14
- }
15
-
16
- .Modal_Close {
17
- font-size: 150%;
18
- cursor: pointer;
19
- line-height: 1;
20
- }
13
+ animation: fade-in var(--duration);
21
14
 
22
- .Modal_Close:hover {
23
- opacity: .8;
24
- text-decoration: none;
15
+ & > * {
16
+ min-width: 250px;
17
+ }
25
18
  }
26
19
 
27
20
  .Modal-fixed {
@@ -1,28 +1,36 @@
1
1
  .Navigation {
2
- --indent: .7em;
3
- display: grid;
2
+ --indent: 0.7em;
3
+ display: flex;
4
+ flex-flow: column;
5
+ flex-grow: 1;
4
6
  align-content: start;
5
7
  }
6
8
 
7
9
  .Navigation_Link {
8
- padding: .7em;
10
+ padding: 0.7em;
9
11
  position: relative;
10
12
  color: inherit;
11
13
  display: flex;
12
14
  align-items: center;
13
15
  }
14
16
 
17
+ .Navigation_Link-bottom {
18
+ margin-top: auto;
19
+ }
15
20
 
16
21
  .Navigation_Link:is(a):hover,
17
- .Navigation-current,
22
+ .Navigation_Link-current,
23
+ .Navigation_Link-ancestor,
18
24
  .Navigation_Link.current {
19
25
  text-decoration: none;
26
+
20
27
  &::after {
21
- content: '';
28
+ content: "";
22
29
  display: block;
23
30
  position: absolute;
24
31
  inset: 3px 0;
25
32
  border-radius: var(--control-radius);
33
+ box-shadow: inset rgb(0 0 0 / 0.2) 0 -2px;
26
34
  background: var(--hover);
27
35
  z-index: -1;
28
36
  }
@@ -2,10 +2,17 @@
2
2
  container: Page;
3
3
  display: flex;
4
4
  flex-flow: wrap;
5
- justify-content: stretch;
5
+ justify-content: start;
6
6
  gap: var(--gap);
7
7
  background: var(--background);
8
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);
9
16
  }
10
17
 
11
18
  .Page_Head {
@@ -18,6 +25,10 @@
18
25
  left: var(--gap);
19
26
  }
20
27
 
28
+ .Page_Foot {
29
+ width: 100%;
30
+ }
31
+
21
32
  .Page_Title {
22
33
  margin: 0;
23
34
  }
@@ -28,21 +39,48 @@
28
39
  margin-left: auto;
29
40
  }
30
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
+
31
54
  .Page_Aside {
32
55
  min-width: 300px;
56
+ max-width: 720px;
33
57
  display: flex;
34
58
  flex-flow: column;
35
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
+ }
36
71
  }
37
72
 
38
73
  .Page_Body {
39
- flex: 1 1 20px;
74
+ flex: 1 1;
75
+ width: 0;
40
76
  display: flex;
41
77
  flex-flow: column;
42
78
  gap: var(--gap);
79
+ /* overflow: auto; */
43
80
  }
44
81
 
45
- .Page-slim .Page_Body {
82
+ .Page-slim .Page_Body,
83
+ .Page_Body-slim {
46
84
  max-width: 720px;
47
85
  }
48
86
 
@@ -62,5 +100,6 @@
62
100
  .Page_Aside,
63
101
  .Page_Foot {
64
102
  justify-self: stretch;
103
+ width: auto;
65
104
  }
66
105
  }
@@ -4,10 +4,8 @@
4
4
  align-items: stretch;
5
5
  box-shadow: var(--box-shadow);
6
6
  border-radius: var(--radius);
7
- width: 100%;
8
- overflow: clip;
9
- position: relative;
10
- z-index: 0;
7
+ background: var(--card);
8
+ min-width: min-content;
11
9
  }
12
10
 
13
11
  .Table_Head,
@@ -19,60 +17,29 @@
19
17
  grid-column: 1/-1;
20
18
  }
21
19
 
22
- .Table_Body {
23
- background: var(--card);
24
- }
25
-
26
20
  .Table_Head {
27
- /*overflow: hidden;*/
28
21
  z-index: 2;
29
22
  white-space: nowrap;
30
23
  font-weight: bold;
31
24
  position: relative;
32
-
33
- &::before, &::after {
34
- content: "";
35
- display: block;
36
- position: absolute;
37
- inset: 0;
38
- height: 200%;
39
- pointer-events: none;
40
- }
41
-
42
- &::before {
43
- backdrop-filter: blur(20px);
44
- mask-image: linear-gradient(
45
- to bottom,
46
- black 0 50%,
47
- transparent 50% 100%
48
- );
49
- }
50
-
51
- &::after {
52
- backdrop-filter: blur(5px);
53
- background: rgba(38, 51, 54, 0.1);
54
-
55
- mask-image: linear-gradient(
56
- to bottom,
57
- transparent 0 50%,
58
- black 50% 51%,
59
- transparent 51%
60
- );
61
- }
25
+ background: var(--card);
26
+ border-bottom: solid 2px var(--border);
27
+ border-top-left-radius: inherit;
28
+ border-top-right-radius: inherit;
62
29
 
63
30
  .Table_Cell {
64
31
  &:first-child {
65
- border-top-left-radius: var(--radius);
32
+ border-top-left-radius: inherit;
66
33
  }
67
34
 
68
35
  &:last-child {
69
- border-top-right-radius: var(--radius);
36
+ border-top-right-radius: inherit;
70
37
  }
71
38
 
72
39
  &:not(:empty) + &:not(:empty)::before {
73
40
  display: block;
74
41
  cursor: col-resize;
75
- content: '';
42
+ content: "";
76
43
  inset: var(--gap) auto var(--gap) 0;
77
44
  position: absolute;
78
45
  border-left: 1px solid var(--border);
@@ -85,10 +52,15 @@
85
52
  }
86
53
  }
87
54
 
55
+ .Table_Head-sticky {
56
+ position: sticky;
57
+ top: 0;
58
+ }
59
+
88
60
  .Table_Row {
89
61
  align-items: center;
90
62
 
91
- &:not(:hover) .Table-shy {
63
+ &:not(:hover) .Table_Cell-shy {
92
64
  visibility: hidden;
93
65
  }
94
66
 
@@ -98,33 +70,32 @@
98
70
  }
99
71
 
100
72
  .Table_Cell {
73
+ position: relative;
74
+ padding: 0.75em 1em;
75
+ align-items: center;
101
76
  white-space: nowrap;
102
77
  overflow: hidden;
103
78
  text-overflow: ellipsis;
104
- padding: 1em;
105
- position: relative;
106
- display: flex;
107
- align-items: center;
108
79
  gap: 2px;
109
80
 
81
+ > code {
82
+ white-space: nowrap;
83
+ }
84
+
110
85
  &:has(img) {
111
86
  padding-block: 0;
112
87
  }
113
88
 
114
- > img, > a > img {
89
+ > img,
90
+ > a > img {
115
91
  height: 2em;
116
92
  /* margin: 4px; */
117
93
  display: inline-block;
118
94
  border-radius: 99px;
119
95
  max-width: none;
120
96
  }
121
- }
122
97
 
123
- .Table-sticky {
124
- position: sticky;
125
- top: 0;
126
- }
127
-
128
- .Table-controls {
129
- text-align: right;
98
+ &-controls {
99
+ text-align: right;
100
+ }
130
101
  }
@@ -1,7 +1,9 @@
1
1
  @import url("Alert.css");
2
2
  @import url("Article.css");
3
3
  @import url("Button.css");
4
+ @import url("Close.css");
4
5
  @import url("Card.css");
6
+ @import url("Chat.css");
5
7
  @import url("Code.css");
6
8
  @import url("Controls.css");
7
9
  @import url("Error.css");
@@ -12,7 +12,7 @@ module CafeCar
12
12
  def index
13
13
  @examples = view_context.template_glob("cafe_car/examples/ui/*")
14
14
  .map { _1.name.sub(/\..+$/, "") }
15
- .to_h { [_1.camelize, "cafe_car/examples/ui/#{_1}"] }
15
+ .to_h { [ _1.camelize, "cafe_car/examples/ui/#{_1}" ] }
16
16
  end
17
17
 
18
18
  private
@@ -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
@@ -16,29 +16,34 @@ addEventListener("mousedown", event => {
16
16
  })
17
17
 
18
18
  addEventListener("mouseup", event => {
19
- if (event.target === window.mouseDownTarget && event.target.matches(".Modal_Close, .Modal")) {
19
+ let isClose = event.target.closest(".Close") || event.target.matches(".Modal")
20
+ if (isClose && event.target === window.mouseDownTarget) {
20
21
  event.preventDefault()
21
22
  event.stopPropagation()
22
- event.target.closest(".Modal").classList.add("remove")
23
+ event.target.closest(".popup").classList.add("remove")
23
24
  }
24
25
  }, { capture: true })
25
26
 
26
27
  addEventListener("keydown", event => {
27
28
  switch (event.key) {
28
29
  case "Escape":
29
- let modal = event.target.closest(".Modal") ||
30
- last(document.querySelectorAll(".Modal-fixed"))
31
- if (modal) modal.classList.add("remove");
30
+ let popup = event.target.closest(".popup") ||
31
+ last(document.querySelectorAll(".popup"))
32
+ if (popup) popup.classList.add("remove");
32
33
  }
33
34
  })
34
35
 
35
- addEventListener("animationend", event => {
36
- if (event.target.matches(".remove")) event.target.remove()
37
- })
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
+ }
38
44
 
39
- addEventListener("transitionend", event => {
40
- if (event.target.matches(".remove")) event.target.remove()
41
- })
45
+ addEventListener("animationend", animationEnd)
46
+ addEventListener("transitionend", animationEnd)
42
47
 
43
48
  // NOTE: field-sizing property is used instead
44
49
  // function adjustHeight(textarea) {