easyfaa 0.20.0 → 1.0.0

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 (207) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +131 -468
  3. data/README.md +49 -222
  4. data/app/assets/fonts/easy/outfit-latin-ext.woff2 +0 -0
  5. data/app/assets/fonts/easy/outfit-latin.woff2 +0 -0
  6. data/app/assets/images/easy/favicon.ico +0 -0
  7. data/app/assets/images/easy/favicon.svg +11 -0
  8. data/app/assets/images/easy/sprite.svg +52 -0
  9. data/app/assets/javascripts/easy/add_rows.js +38 -0
  10. data/app/assets/javascripts/easy/flyer.js +100 -0
  11. data/app/assets/javascripts/easy/highlight.js +54 -0
  12. data/app/assets/javascripts/easy/konami.js +73 -0
  13. data/app/assets/javascripts/easy/modal.js +49 -0
  14. data/app/assets/javascripts/easy/pin_code.js +43 -0
  15. data/app/assets/javascripts/easy/report_sort.js +57 -0
  16. data/app/assets/javascripts/easy/rotating_placeholder.js +35 -0
  17. data/app/assets/javascripts/easy/step_foot.js +17 -0
  18. data/app/assets/javascripts/easy/switches.js +96 -0
  19. data/app/assets/javascripts/easy/switches_address.js +39 -0
  20. data/app/assets/javascripts/easy/tabs.js +35 -0
  21. data/app/assets/javascripts/easy/theme.js +41 -0
  22. data/app/assets/stylesheets/easy/base.css +42 -0
  23. data/app/assets/stylesheets/easy/components.css +533 -0
  24. data/app/assets/stylesheets/easy/layout.css +47 -0
  25. data/app/assets/stylesheets/easy/tokens.css +11 -0
  26. data/app/controllers/easyfaa/errors_controller.rb +41 -0
  27. data/app/helpers/easyfaa/easy_helper.rb +58 -0
  28. data/app/models/aircraft_avatar.rb +61 -0
  29. data/app/views/easy/_ad_card.html.erb +16 -0
  30. data/app/views/easy/_ad_list.html.erb +11 -0
  31. data/app/views/easy/_flyer.html.erb +10 -0
  32. data/app/views/easy/_logo.html.erb +15 -0
  33. data/app/views/easy/_modal.html.erb +17 -0
  34. data/app/views/easy/_part_tabs.html.erb +24 -0
  35. data/app/views/easy/_pin_code.html.erb +10 -0
  36. data/app/views/easy/_report_head.html.erb +28 -0
  37. data/app/views/easy/mailer/_button.html.erb +10 -0
  38. data/app/views/easy/mailer/_card.html.erb +23 -0
  39. data/app/views/easy/mailer/_code.html.erb +28 -0
  40. data/app/views/easy/mailer/_code.text.erb +19 -0
  41. data/app/views/easy/mailer/_frame.html.erb +35 -0
  42. data/app/views/easy/mailer/_text.html.erb +7 -0
  43. data/app/views/easyfaa/errors/show.html.erb +17 -0
  44. data/app/views/layouts/easy/site.html.erb +55 -0
  45. data/app/views/layouts/easyfaa/errors.html.erb +30 -0
  46. data/config/aircraft_avatars.yml +26 -0
  47. data/lib/easyfaa/engine.rb +16 -43
  48. data/lib/easyfaa/version.rb +2 -2
  49. data/lib/easyfaa.rb +14 -35
  50. metadata +52 -181
  51. data/app/views/layouts/easyfaa.html.erb +0 -24
  52. data/app/views/recourses/_head.html.erb +0 -3
  53. data/art/autopilot.svg +0 -9
  54. data/art/cube.svg +0 -9
  55. data/art/design.svg +0 -8
  56. data/art/down.html.erb +0 -95
  57. data/art/easyfaa.svg +0 -7
  58. data/art/fountain.svg +0 -9
  59. data/art/goldrush.svg +0 -7
  60. data/art/houston.svg +0 -8
  61. data/art/icons.txt +0 -185
  62. data/art/index.html.erb +0 -157
  63. data/config/locales/easyfaa.en.yml +0 -7
  64. data/lib/easyfaa/copy.rb +0 -5
  65. data/lib/easyfaa/down.rb +0 -19
  66. data/lib/easyfaa/helpers/flows.rb +0 -53
  67. data/lib/easyfaa/helpers/heads.rb +0 -60
  68. data/lib/easyfaa/helpers/logos.rb +0 -43
  69. data/lib/easyfaa/helpers.rb +0 -11
  70. data/lib/easyfaa/marks.rb +0 -16
  71. data/public/404.html +0 -118
  72. data/public/_headers +0 -28
  73. data/public/bh/css/bh.css +0 -1
  74. data/public/bh/js/bh.js +0 -152
  75. data/public/bh/theme/bootstrap.css +0 -10
  76. data/public/bh/theme/dawn.css +0 -189
  77. data/public/bh/theme/dracula.css +0 -189
  78. data/public/bh/theme/gruvbox.css +0 -189
  79. data/public/bh/theme/monokai.css +0 -190
  80. data/public/bh/theme/nord.css +0 -189
  81. data/public/bh/theme/one_dark.css +0 -189
  82. data/public/bh/theme/solarized.css +0 -188
  83. data/public/bh/theme/tokyo_night.css +0 -189
  84. data/public/css/easyfaa.css +0 -1
  85. data/public/down/autopilot.html +0 -87
  86. data/public/down/cube.html +0 -87
  87. data/public/down/design.html +0 -87
  88. data/public/down/easyfaa.html +0 -84
  89. data/public/down/fountain.html +0 -87
  90. data/public/down/goldrush.html +0 -87
  91. data/public/down/houston.html +0 -87
  92. data/public/favicon/autopilot-180.png +0 -0
  93. data/public/favicon/autopilot-192.png +0 -0
  94. data/public/favicon/autopilot-512.png +0 -0
  95. data/public/favicon/autopilot-96.png +0 -0
  96. data/public/favicon/autopilot-dev.svg +0 -9
  97. data/public/favicon/autopilot.ico +0 -0
  98. data/public/favicon/autopilot.svg +0 -9
  99. data/public/favicon/autopilot.webmanifest +0 -12
  100. data/public/favicon/cube-180.png +0 -0
  101. data/public/favicon/cube-192.png +0 -0
  102. data/public/favicon/cube-512.png +0 -0
  103. data/public/favicon/cube-96.png +0 -0
  104. data/public/favicon/cube-dev.svg +0 -9
  105. data/public/favicon/cube.ico +0 -0
  106. data/public/favicon/cube.svg +0 -9
  107. data/public/favicon/cube.webmanifest +0 -12
  108. data/public/favicon/design-180.png +0 -0
  109. data/public/favicon/design-192.png +0 -0
  110. data/public/favicon/design-512.png +0 -0
  111. data/public/favicon/design-96.png +0 -0
  112. data/public/favicon/design-dev.svg +0 -8
  113. data/public/favicon/design.ico +0 -0
  114. data/public/favicon/design.svg +0 -8
  115. data/public/favicon/design.webmanifest +0 -12
  116. data/public/favicon/easyfaa-180.png +0 -0
  117. data/public/favicon/easyfaa-192.png +0 -0
  118. data/public/favicon/easyfaa-512.png +0 -0
  119. data/public/favicon/easyfaa-96.png +0 -0
  120. data/public/favicon/easyfaa-dev.svg +0 -7
  121. data/public/favicon/easyfaa.ico +0 -0
  122. data/public/favicon/easyfaa.svg +0 -7
  123. data/public/favicon/easyfaa.webmanifest +0 -12
  124. data/public/favicon/fountain-180.png +0 -0
  125. data/public/favicon/fountain-192.png +0 -0
  126. data/public/favicon/fountain-512.png +0 -0
  127. data/public/favicon/fountain-96.png +0 -0
  128. data/public/favicon/fountain-dev.svg +0 -9
  129. data/public/favicon/fountain.ico +0 -0
  130. data/public/favicon/fountain.svg +0 -9
  131. data/public/favicon/fountain.webmanifest +0 -12
  132. data/public/favicon/goldrush-180.png +0 -0
  133. data/public/favicon/goldrush-192.png +0 -0
  134. data/public/favicon/goldrush-512.png +0 -0
  135. data/public/favicon/goldrush-96.png +0 -0
  136. data/public/favicon/goldrush-dev.svg +0 -7
  137. data/public/favicon/goldrush.ico +0 -0
  138. data/public/favicon/goldrush.svg +0 -7
  139. data/public/favicon/goldrush.webmanifest +0 -12
  140. data/public/favicon/houston-180.png +0 -0
  141. data/public/favicon/houston-192.png +0 -0
  142. data/public/favicon/houston-512.png +0 -0
  143. data/public/favicon/houston-96.png +0 -0
  144. data/public/favicon/houston-dev.svg +0 -8
  145. data/public/favicon/houston.ico +0 -0
  146. data/public/favicon/houston.svg +0 -8
  147. data/public/favicon/houston.webmanifest +0 -12
  148. data/public/favicon.ico +0 -0
  149. data/public/font/bootstrap-icons-6LILWPE2.woff2 +0 -0
  150. data/public/font/bootstrap-icons-FRBPO3H7.woff2 +0 -0
  151. data/public/font/ibm-plex-sans-500-latin-LA7QXWWL.woff2 +0 -0
  152. data/public/font/ibm-plex-sans-500-latin-ext-X4QHZICY.woff2 +0 -0
  153. data/public/font/ibm-plex-sans-500-latin-ext.woff2 +0 -0
  154. data/public/font/ibm-plex-sans-500-latin.woff2 +0 -0
  155. data/public/font/ibm-plex-sans-500-vietnamese-24B6BSND.woff2 +0 -0
  156. data/public/font/ibm-plex-sans-500-vietnamese.woff2 +0 -0
  157. data/public/index.html +0 -165
  158. data/public/js/easyfaa.js +0 -1
  159. data/public/logo/autopilot.png +0 -0
  160. data/public/logo/autopilot.svg +0 -9
  161. data/public/logo/cube.png +0 -0
  162. data/public/logo/cube.svg +0 -9
  163. data/public/logo/design.png +0 -0
  164. data/public/logo/design.svg +0 -8
  165. data/public/logo/easyfaa.png +0 -0
  166. data/public/logo/easyfaa.svg +0 -7
  167. data/public/logo/fountain.png +0 -0
  168. data/public/logo/fountain.svg +0 -9
  169. data/public/logo/goldrush.png +0 -0
  170. data/public/logo/goldrush.svg +0 -7
  171. data/public/logo/houston.png +0 -0
  172. data/public/logo/houston.svg +0 -8
  173. data/public/page/400.html +0 -118
  174. data/public/page/404.html +0 -118
  175. data/public/page/406-unsupported-browser.html +0 -118
  176. data/public/page/422.html +0 -118
  177. data/public/page/500.html +0 -118
  178. data/public/site.webmanifest +0 -12
  179. data/public/theme/bootstrap.css +0 -10
  180. data/public/theme/dawn.css +0 -189
  181. data/public/theme/dracula.css +0 -189
  182. data/public/theme/gruvbox.css +0 -189
  183. data/public/theme/monokai.css +0 -190
  184. data/public/theme/nord.css +0 -189
  185. data/public/theme/one_dark.css +0 -189
  186. data/public/theme/solarized.css +0 -188
  187. data/public/theme/tokyo_night.css +0 -189
  188. data/public/v0.20.0/bh/css/bh.css +0 -1
  189. data/public/v0.20.0/bh/js/bh.js +0 -152
  190. data/public/v0.20.0/bh/theme/bootstrap.css +0 -10
  191. data/public/v0.20.0/bh/theme/dawn.css +0 -189
  192. data/public/v0.20.0/bh/theme/dracula.css +0 -189
  193. data/public/v0.20.0/bh/theme/gruvbox.css +0 -189
  194. data/public/v0.20.0/bh/theme/monokai.css +0 -190
  195. data/public/v0.20.0/bh/theme/nord.css +0 -189
  196. data/public/v0.20.0/bh/theme/one_dark.css +0 -189
  197. data/public/v0.20.0/bh/theme/solarized.css +0 -188
  198. data/public/v0.20.0/bh/theme/tokyo_night.css +0 -189
  199. data/public/v0.20.0/css/easyfaa.css +0 -1
  200. data/public/v0.20.0/font/bootstrap-icons-FRBPO3H7.woff2 +0 -0
  201. data/public/v0.20.0/font/ibm-plex-sans-500-latin-LA7QXWWL.woff2 +0 -0
  202. data/public/v0.20.0/font/ibm-plex-sans-500-latin-ext-X4QHZICY.woff2 +0 -0
  203. data/public/v0.20.0/font/ibm-plex-sans-500-latin-ext.woff2 +0 -0
  204. data/public/v0.20.0/font/ibm-plex-sans-500-latin.woff2 +0 -0
  205. data/public/v0.20.0/font/ibm-plex-sans-500-vietnamese-24B6BSND.woff2 +0 -0
  206. data/public/v0.20.0/font/ibm-plex-sans-500-vietnamese.woff2 +0 -0
  207. data/public/v0.20.0/js/easyfaa.js +0 -1
@@ -0,0 +1,11 @@
1
+ /* The easy design system's tokens: every colour any easy app draws with, light and dark. The
2
+ dark ones follow the system unless the visitor picks a theme (data-theme on <html>). Plain hex:
3
+ every page has to work on old iPhones, Androids and Windows browsers. */
4
+ :root{color-scheme:light;--color-bg:#ffffff;--color-surface-1:#f6f7f8;--color-surface-2:#f1f2f3;--color-surface-3:#e2e5e7;--color-text:#2c2f32;--color-text-2:#50575d;--color-text-muted:#636c74;--color-line:#c6cbcf;--color-primary:#1a70d0;--color-primary-strong:#12559f;--shadow-raise:0 1px 0 rgba(11,30,58,.10),0 1px 3px rgba(11,30,58,.12);--shadow-raise-hover:0 2px 0 rgba(11,30,58,.10),0 4px 10px rgba(11,30,58,.16);--shadow-sunk:inset 0 1px 2px rgba(11,30,58,.10);--color-on-primary:#ffffff;--color-primary-soft:#dfe5ff;--color-on-primary-soft:#215aa5;--color-ok:#19753e;--color-bad:#b3261e;--color-bad-bg:#fbe3e0;--color-bad-line:#ee9b93;--color-ok-bg:#d8f0dd;--color-ok-line:#87d29a;--color-warn:#7e6317;--color-warn-bg:#fff4d5;--color-warn-line:#ffde80;--color-engine-bg:#f3e6d8;--color-engine:#6f4219;--color-engine-line:#c08a57;--color-propeller-bg:#ece4f7;--color-propeller:#5b2a86;--color-propeller-line:#a47fd0;--color-appliance-bg:#d9f0ec;--color-appliance:#1b5e55;--color-appliance-line:#6fb3a8;--color-midnight:#0b1e3a;--color-sky:#5aa9e6;--color-focus:#1a70d0;--color-sky-band-top:#eaf3fb;--color-sky-band-bottom:#ffffff;}
5
+ @media (prefers-color-scheme: dark){
6
+ :root:not([data-theme="light"]){color-scheme:dark;--color-bg:#131415;--color-surface-1:#202223;--color-surface-2:#2c2f32;--color-surface-3:#3d4347;--color-text:#f1f2f3;--color-text-2:#abb2b8;--color-text-muted:#8c96a0;--color-line:#50575d;--color-primary:#689dff;--color-primary-strong:#9dbfff;--shadow-raise:0 1px 0 rgba(0,0,0,.5),0 1px 3px rgba(0,0,0,.5);--shadow-raise-hover:0 2px 0 rgba(0,0,0,.5),0 4px 10px rgba(0,0,0,.6);--shadow-sunk:inset 0 1px 2px rgba(0,0,0,.45);--color-on-primary:#131415;--color-primary-soft:#1d3155;--color-on-primary-soft:#bccdff;--color-ok:#59c27a;--color-bad:#ff8a80;--color-bad-bg:#4a1d1a;--color-bad-line:#c4453a;--color-ok-bg:#173e24;--color-ok-line:#13924c;--color-warn:#ffd352;--color-warn-bg:#564415;--color-warn-line:#d2a511;--color-engine-bg:#3d2a1a;--color-engine:#e8c29c;--color-engine-line:#9a6a3a;--color-propeller-bg:#2e1f44;--color-propeller:#d6bdf2;--color-propeller-line:#7a55a8;--color-appliance-bg:#16332f;--color-appliance:#9fd9cf;--color-appliance-line:#3f8a7f;--color-midnight:#cfe3f6;--color-sky:#5aa9e6;--color-focus:#689dff;--color-sky-band-top:#0c2338;--color-sky-band-bottom:#131415;}
7
+ }
8
+ :root[data-theme="dark"]{color-scheme:dark;--color-bg:#131415;--color-surface-1:#202223;--color-surface-2:#2c2f32;--color-surface-3:#3d4347;--color-text:#f1f2f3;--color-text-2:#abb2b8;--color-text-muted:#8c96a0;--color-line:#50575d;--color-primary:#689dff;--color-primary-strong:#9dbfff;--shadow-raise:0 1px 0 rgba(0,0,0,.5),0 1px 3px rgba(0,0,0,.5);--shadow-raise-hover:0 2px 0 rgba(0,0,0,.5),0 4px 10px rgba(0,0,0,.6);--shadow-sunk:inset 0 1px 2px rgba(0,0,0,.45);--color-on-primary:#131415;--color-primary-soft:#1d3155;--color-on-primary-soft:#bccdff;--color-ok:#59c27a;--color-bad:#ff8a80;--color-bad-bg:#4a1d1a;--color-bad-line:#c4453a;--color-ok-bg:#173e24;--color-ok-line:#13924c;--color-warn:#ffd352;--color-warn-bg:#564415;--color-warn-line:#d2a511;--color-engine-bg:#3d2a1a;--color-engine:#e8c29c;--color-engine-line:#9a6a3a;--color-propeller-bg:#2e1f44;--color-propeller:#d6bdf2;--color-propeller-line:#7a55a8;--color-appliance-bg:#16332f;--color-appliance:#9fd9cf;--color-appliance-line:#3f8a7f;--color-midnight:#cfe3f6;--color-sky:#5aa9e6;--color-focus:#689dff;--color-sky-band-top:#0c2338;--color-sky-band-bottom:#131415;}
9
+ /* The one width every page, its header and its footer are capped at: what an account's page reaches, its 280px
10
+ sidebar beside 64rem of content and its 32px gutters (1368px). */
11
+ :root{--page-max:85.5rem}
@@ -0,0 +1,41 @@
1
+ module EasyFAA
2
+ # The pages every app shows for 400, 404, 422 and 500 (config.exceptions_app, set by the
3
+ # engine). Plain ActionController::Base, not the host's ApplicationController: a 500 is exactly
4
+ # the moment the database or a session might be what broke, so nothing here may depend on either.
5
+ class ErrorsController < ActionController::Base
6
+ layout 'easyfaa/errors'
7
+ before_action :set_site
8
+
9
+ # Heading and sentence by status.
10
+ MESSAGES = {
11
+ 400 => ['Bad request', 'The request could not be understood.'],
12
+ 404 => ['Page not found', 'You may have mistyped the address, or the page may have moved.'],
13
+ 422 => ['Can’t process that', 'The change was rejected, often because a page had been ' \
14
+ 'left open too long. Going back and trying again usually ' \
15
+ 'fixes it.',],
16
+ 500 => ['Something went wrong', 'We’ve been notified and are looking into it. Try again ' \
17
+ 'in a moment.',],
18
+ }.freeze
19
+
20
+ # The page for the status the route names, whatever format was asked for.
21
+ def show
22
+ @status = params[:status].to_i
23
+ @heading, @message = MESSAGES.fetch @status, MESSAGES.fetch(500)
24
+ render status: @status, formats: :html
25
+ end
26
+
27
+ # The page shown while the app is down for maintenance.
28
+ def down
29
+ @heading = 'Down for maintenance'
30
+ @message = "#{@site_name} is being updated and will be back in a few minutes."
31
+ render :show, formats: :html
32
+ end
33
+
34
+ private
35
+
36
+ def set_site
37
+ # The app names the site for the request (EasyFAA.site).
38
+ @site_name, @site_url = EasyFAA.site.call request
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,58 @@
1
+ module EasyFAA
2
+ # The easy design system's icons, for every view of every app (EasyFAA::Engine): one sprite every
3
+ # page shares (app/assets/images/easy/sprite.svg), one file the browser keeps for a year rather
4
+ # than a copy in every page.
5
+ module EasyHelper
6
+ # How an AD reaches an aircraft, as its chip says it: the word, then its icon in the sprite.
7
+ LEVELS = {
8
+ 'applies' => %w[Applies applies], 'may_apply' => ['May apply', 'maybe'],
9
+ 'not_applying' => ['Doesn’t apply', 'none'], 'superseded' => %w[Superseded superseded],
10
+ 'reaches' => %w[Affects applies],
11
+ }.freeze
12
+
13
+ # Where an icon is in the sprite: its address, then the icon's id after the #.
14
+ # @param name [String] "sun", "newest": its id in the sprite, without the i-.
15
+ # @param kind ["i", "s"] an icon of the design system's, or a social network's.
16
+ def easy_icon_href(name, kind = 'i') = "#{asset_path 'easy/sprite.svg'}##{kind}-#{name}"
17
+
18
+ # An icon, hidden from screen readers: the text beside it says what it is.
19
+ # @param href [String] easy_icon_href's answer, or a name in the sprite.
20
+ # @param class [String] the svg's classes, "i" for an icon drawn the size of the text around it.
21
+ def easy_icon(href, class: 'i')
22
+ href = easy_icon_href(href) unless href.include? '#'
23
+ # Both hrefs: the old Safaris every page is meant for read only xlink:href.
24
+ tag.svg(tag.use('xlink:href': href, href:), class:, aria: { hidden: true }, focusable: false)
25
+ end
26
+
27
+ # The × of what can be removed (removable-x), drawn, not typed: it stays a cross in any font.
28
+ def easy_x
29
+ tag.svg(tag.path(d: 'M1 1l10 10M11 1L1 11', stroke: 'currentColor', 'stroke-width': 2, 'stroke-linecap': 'round'),
30
+ viewBox: '0 0 12 12', width: 12, height: 12, aria: { hidden: true }, focusable: false)
31
+ end
32
+
33
+ # An AD's level on its card, the level's color behind its icon and word.
34
+ # @param level [String] "applies", "may_apply", "not_applying", "superseded" or "reaches".
35
+ def easy_level_chip(level)
36
+ word, icon = LEVELS.fetch level.to_s
37
+ tag.span safe_join([easy_icon(icon), word]), class: "level-chip #{level}"
38
+ end
39
+
40
+ # An email's fonts, for a style attribute: no stylesheet reaches an email, so every piece names its own.
41
+ # @param kind [:body, :heading] the system's font for words, or Outfit where it loads for a title.
42
+ def easy_mailer_font(kind = :body)
43
+ if kind == :heading
44
+ %(font-family:"Outfit","Helvetica Neue",Helvetica,Arial,sans-serif;)
45
+ else
46
+ %(font-family:-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;)
47
+ end
48
+ end
49
+
50
+ # An aircraft's avatar until it has a picture: its silhouette in a circle, like a contact's.
51
+ # @param name [String] AircraftAvatar.for's answer, "high-wing-tailwheel".
52
+ # @param size [Integer] the circle's width and height in pixels.
53
+ def easy_aircraft_avatar(name, size: 40)
54
+ icon = easy_icon easy_icon_href(name, 'a'), class: nil
55
+ tag.span icon, class: 'aircraft-avatar', style: "width:#{size}px;height:#{size}px"
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,61 @@
1
+ # Which of the 20 aircraft avatars (sprite a-*, easy_aircraft_avatar) draws an aircraft before
2
+ # anyone has a picture of it: from what the registry says it is, and for a single-engine airplane
3
+ # from its make and model too (config/aircraft_avatars.yml), so a Cub, a 172 and a Bonanza differ.
4
+ module AircraftAvatar
5
+ # The single-engine airplane families by make and model, in the order they are tried.
6
+ FAMILIES = YAML.load_file(File.expand_path('../../config/aircraft_avatars.yml', __dir__))
7
+ .transform_values { Regexp.new(it.join('|'), Regexp::IGNORECASE) }.freeze
8
+
9
+ # The engine types that turn no propeller and no rotor of their own.
10
+ JETS = %w[turbo_jet turbo_fan].freeze
11
+
12
+ # The families a single turboprop keeps (an Air Tractor), rather than being drawn as one.
13
+ TURBOPROP_FAMILIES = %w[ag-plane fixed-wing-drone].freeze
14
+
15
+ # @param type [String] the registry's type of aircraft: "fixed_wing_single_engine", "rotorcraft"…
16
+ # @param engine [String, nil] the registry's type of engine: "reciprocating", "electric"…
17
+ # @param name [String, nil] its make and model, "Piper PA-18-150".
18
+ # @param engines [Integer, nil] how many engines its model has.
19
+ # @param seats [Integer, nil] how many seats its model has.
20
+ # @param airliner [Boolean] whether its model is an airliner (30 seats or more, or its freighter).
21
+ # @return [String] the avatar's name in the sprite, without its a-: "high-wing-tailwheel".
22
+ def self.for(type:, engine: nil, name: nil, engines: nil, seats: nil, airliner: false)
23
+ case type.to_s
24
+ when 'balloon', 'blimp' then 'balloon'
25
+ when 'glider' then 'glider'
26
+ when 'weight_shift_control' then 'trike'
27
+ when 'powered_parachute' then 'powered-parachute'
28
+ when 'hybrid_lift' then 'fixed-wing-drone'
29
+ when 'rotorcraft', 'gyroplane' then rotorcraft(engine, engines.to_i, seats.to_i)
30
+ when 'fixed_wing_multi_engine' then multi_engine(engine, airliner)
31
+ else single_engine engine, name.to_s
32
+ end
33
+ end
34
+
35
+ class << self
36
+ private
37
+
38
+ def rotorcraft(engine, engines, seats)
39
+ return 'multirotor-drone' if engine == 'electric'
40
+
41
+ engines > 1 || seats > 8 ? 'heavy-helicopter' : 'helicopter'
42
+ end
43
+
44
+ def multi_engine(engine, airliner)
45
+ return 'twin' unless JETS.include? engine
46
+
47
+ airliner ? 'airliner' : 'business-jet'
48
+ end
49
+
50
+ def single_engine(engine, name)
51
+ return 'business-jet' if JETS.include? engine
52
+
53
+ family = FAMILIES.find { |_, pattern| pattern.match?(name) }&.first
54
+ if family && (engine != 'turbo_prop' || TURBOPROP_FAMILIES.include?(family))
55
+ family
56
+ else
57
+ engine == 'turbo_prop' ? 'single-turboprop' : 'high-wing'
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,16 @@
1
+ <%# locals: (number:, href:, subject: nil, title: nil, part: nil, level: nil, kind: nil, tie: nil, effective: nil, data: {}) -%>
2
+ <%# One AD in an ad-card-list: its level, kind, tie and date, then its number and subject as a link over its title; what
3
+ else the page says of it is the block's (render layout: "easy/ad_card"). `part` is what of the aircraft it reaches
4
+ ("airframe", "engine", "propeller", "appliance"), `level` one of easy_level_chip's, `effective` a date. The card
5
+ carries its number as data-sort, for the button that flips a list (easy/report_sort). -%>
6
+ <%= tag.li class: [ "ad-card", part, level ], data: { sort: number.to_s, **data } do %>
7
+ <div class="ad-card-top">
8
+ <%= easy_level_chip level if level %>
9
+ <% if kind %><span class="ad-card-kind"><%= kind %></span><% end %>
10
+ <% if tie %><span class="tie-chip"><%= tie %></span><% end %>
11
+ <% if effective %><span class="ad-card-effective">Effective <%= effective.strftime "%B %-d, %Y" %></span><% end %>
12
+ </div>
13
+ <% name = [ "AD #{number}", subject.presence ].compact.join(": ") -%>
14
+ <h4><%= link_to name, href %><% if title.present? %><span class="t"><%= title %></span><% end %></h4>
15
+ <%= yield %>
16
+ <% end %>
@@ -0,0 +1,11 @@
1
+ <%# locals: (none:, size:, any: true) -%>
2
+ <%# A list of ADs (render layout: "easy/ad_list"): the block draws its cards (easy/ad_card), under the line that says
3
+ its switches and its search leave none, which easy/switches shows and hides. With no card at all it is one line
4
+ and no list. none: what to say then, "No AD affects N714AD through its propeller."; size: how many cards the
5
+ block draws; any: whether one shows as the page arrives, false when every one starts switched off. -%>
6
+ <% if size.zero? %>
7
+ <p class="none-here"><%= none %></p>
8
+ <% else %>
9
+ <p class="none-here js-none"<%= " hidden".html_safe if any %>>Nothing matches the filters above.</p>
10
+ <ol class="ad-card-list"><%= yield %></ol>
11
+ <% end %>
@@ -0,0 +1,10 @@
1
+ <%# locals: (plane:, date: nil) -%>
2
+ <%# A plane crossing the sky behind a hero, its trail behind it, flown by easy/flyer.js, which the page includes once,
3
+ with easy/konami.js, its easter egg.
4
+ plane: its icon in the sprite, "plane" (easyFAA's airliner), "small-plane" (easyAnnual's single engine) or
5
+ "twin-plane" (easyFAA's accidents' twin engine).
6
+ date: "2026-12-25", the day to fly on instead of the visitor's today, to see a holiday's colors. -%>
7
+ <%= tag.div class: "sky js-sky", aria: { hidden: true }, data: { date: } do %>
8
+ <svg class="trail js-trail" focusable="false"></svg>
9
+ <span class="flyer js-flyer"><%= easy_icon plane %></span>
10
+ <% end %>
@@ -0,0 +1,15 @@
1
+ <%# locals: (name:, width:) -%>
2
+ <%# An app’s logo, drawn inline so its plane can wiggle on hover and its ink follow the theme: the paper
3
+ plane, then "easy", then the rest of the name, which the app hands over as the paths of its letters
4
+ (render layout: "easy/logo"). Its width is the drawing’s, on a height of 56; it is shown 32 tall. -%>
5
+ <svg class="logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 <%= width %> 56" width="<%= (width * 32 / 56.0).round %>" height="32" role="img" aria-label="<%= name %>">
6
+ <g class="plane"><g transform="scale(0.875)"><g class="ink-stroke" stroke-width="2.5" stroke-linejoin="round">
7
+ <polygon points="60,8 4,30 25,37" fill="#ffffff"/>
8
+ <polygon points="60,8 25,37 31,57" fill="#cfe0f3"/>
9
+ </g>
10
+ <g stroke="#7a93b3" stroke-width="1.6" stroke-linecap="round">
11
+ <line x1="16" y1="28.5" x2="36" y2="21"/><line x1="21" y1="31.5" x2="44" y2="22.5"/><line x1="27" y1="33" x2="46" y2="25.5"/>
12
+ </g>
13
+ <path d="M10 44 C14 50 18 53 24 55" fill="none" class="ink-stroke" stroke-width="2" stroke-dasharray="3 3" stroke-linecap="round"/></g></g>
14
+ <g class="ink"><path d="M79.43 43.199999999999996Q76.58 43.199999999999996 74.31 41.91Q72.04 40.62 70.72 38.364999999999995Q69.4 36.11 69.4 33.29Q69.4 30.479999999999997 70.695 28.244999999999997Q71.99 26.009999999999998 74.22 24.705Q76.45 23.399999999999995 79.19 23.399999999999995Q81.82 23.399999999999995 83.84 24.604999999999997Q85.86 25.809999999999995 87.015 27.919999999999995Q88.17 30.029999999999998 88.17 32.739999999999995Q88.17 33.14 88.12 33.599999999999994Q88.07 34.059999999999995 87.96000000000001 34.64H71.93V31.65H86.05L84.73 32.8Q84.71000000000001 30.889999999999997 84.04 29.54Q83.37 28.189999999999998 82.13 27.455Q80.89 26.719999999999995 79.12 26.719999999999995Q77.26 26.719999999999995 75.875 27.525Q74.49 28.33 73.725 29.769999999999996Q72.96 31.209999999999997 72.96 33.17999999999999Q72.96 35.169999999999995 73.76499999999999 36.669999999999995Q74.57 38.169999999999995 76.03999999999999 38.995Q77.51 39.82 79.44 39.82Q81.03 39.82 82.38 39.25Q83.73 38.68 84.67 37.56999999999999L86.98 39.91Q85.62 41.51 83.64500000000001 42.355Q81.67 43.199999999999996 79.43 43.199999999999996Z"/><path d="M100.17 43.199999999999996Q97.53999999999999 43.199999999999996 95.42999999999999 41.894999999999996Q93.32 40.589999999999996 92.095 38.355Q90.87 36.12 90.87 33.339999999999996Q90.87 30.529999999999998 92.095 28.29Q93.32 26.049999999999997 95.425 24.724999999999994Q97.53 23.399999999999995 100.15 23.399999999999995Q102.33 23.399999999999995 104.03 24.299999999999997Q105.72999999999999 25.199999999999996 106.76499999999999 26.789999999999996Q107.8 28.379999999999995 107.91 30.439999999999998V36.15Q107.78999999999999 38.209999999999994 106.77 39.80499999999999Q105.75 41.4 104.045 42.3Q102.34 43.199999999999996 100.17 43.199999999999996ZM100.75 39.79Q103.44 39.79 105.07499999999999 37.985Q106.71 36.18 106.71 33.309999999999995Q106.71 31.379999999999995 105.965 29.914999999999996Q105.22 28.449999999999996 103.875 27.629999999999995Q102.53 26.809999999999995 100.73 26.809999999999995Q98.92999999999999 26.809999999999995 97.55 27.639999999999997Q96.17 28.47 95.375 29.935Q94.58 31.4 94.58 33.3Q94.58 35.199999999999996 95.375 36.66499999999999Q96.17 38.129999999999995 97.56 38.959999999999994Q98.95 39.79 100.75 39.79ZM106.49 42.8V37.699999999999996L107.15 33.05L106.49 28.459999999999997V23.799999999999997H110.1V42.8Z"/><path d="M121.38 43.199999999999996Q119.81 43.199999999999996 118.375 42.78Q116.94 42.36 115.75 41.59Q114.56 40.82 113.67 39.739999999999995L115.98 37.419999999999995Q117.03 38.68 118.39500000000001 39.3Q119.76 39.919999999999995 121.45 39.919999999999995Q123.13 39.919999999999995 124.03999999999999 39.345Q124.95 38.769999999999996 124.95 37.709999999999994Q124.95 36.669999999999995 124.215 36.089999999999996Q123.48 35.51 122.315 35.135Q121.15 34.76 119.86 34.39Q118.57 34.019999999999996 117.405 33.419999999999995Q116.24 32.81999999999999 115.505 31.764999999999993Q114.77 30.709999999999997 114.77 28.999999999999996Q114.77 27.279999999999998 115.595 26.029999999999998Q116.42 24.779999999999998 117.935 24.089999999999996Q119.45 23.399999999999995 121.55 23.399999999999995Q123.78 23.399999999999995 125.525 24.189999999999998Q127.27 24.979999999999997 128.4 26.539999999999996L126.09 28.849999999999994Q125.26 27.79 124.09 27.235Q122.92 26.679999999999996 121.43 26.679999999999996Q119.88 26.679999999999996 119.05 27.229999999999997Q118.22 27.779999999999998 118.22 28.72Q118.22 29.669999999999995 118.955 30.199999999999996Q119.69 30.729999999999997 120.85499999999999 31.08Q122.02 31.429999999999996 123.31 31.809999999999995Q124.6 32.19 125.765 32.83Q126.93 33.47 127.665 34.55Q128.4 35.629999999999995 128.4 37.4Q128.4 40.07 126.505 41.635Q124.61 43.199999999999996 121.38 43.199999999999996Z"/><path d="M138.68 42.949999999999996 130.45000000000002 23.799999999999997H134.42000000000002L140.54000000000002 38.8H139.26000000000002L145.71 23.799999999999997H149.71L140.84 42.949999999999996ZM133.25 50.76 138.82000000000002 39.29 140.84 42.949999999999996 137.24 50.76Z"/><%= yield %></g>
15
+ </svg>
@@ -0,0 +1,17 @@
1
+ <%# locals: (id:, close:, title: nil, open: false, emoji: nil, classes: nil) -%>
2
+ <%# A dialog over the page, rendered as a layout around what it holds (render layout: "easy/modal" do … end), which
3
+ always lets its reader out: its ×, Esc or a click around it (easy/modal.js, which the page includes once), or
4
+ without the script the × as a link. Shown when its address's #id names it, or open: true (drawn already open, say
5
+ once a form asked).
6
+ close: where the × leads, "#" for a dialog opened by its #id, the page without what opened it otherwise.
7
+ title: its heading; without one, what it holds gives its own, with the id "#{id}-title" (one per step, say).
8
+ classes: the app's own, beside modal, to style what it holds. -%>
9
+ <%= tag.div id:, class: [ "modal", ("is-open" if open), classes ], role: "dialog",
10
+ aria: { modal: true, labelledby: "#{id}-title" } do %>
11
+ <div class="modal-box">
12
+ <%= link_to "×", close, class: "modal-x", aria: { label: "Close" } %>
13
+ <% if title %><h2 id="<%= id %>-title"><% if emoji %><span
14
+ aria-hidden="true"><%= emoji %></span> <% end %><%= title %></h2><% end %>
15
+ <%= yield %>
16
+ </div>
17
+ <% end %>
@@ -0,0 +1,24 @@
1
+ <%# locals: (parts:) -%>
2
+ <%# The parts of an aircraft as tabs over one section each (render layout: "easy/part_tabs"): the block draws what a
3
+ part's section holds, under its heading, and takes the part. Every page that lists ADs part by part is drawn with
4
+ it, so the tabs, the switches and the counts (easy/tabs, easy/switches) find the same page everywhere.
5
+ parts: [{ id: "engine", name: "Engine", icon: "engine", count: 12 }, …]; a part may also say dot: "Due now" for a
6
+ dot after its count, and aside: true to sit at the row's right end, as what is not a part does (Accidents): its
7
+ count is its own, never counted again from the ADs shown. -%>
8
+ <ul class="part-tabs js-tabs">
9
+ <% parts.each_with_index do |part, index| %>
10
+ <%= tag.li class: ("part-tabs-records" if part[:aside]) do %>
11
+ <a href="#<%= part[:id] %>" class="<%= class_names part[:id], "is-on": index.zero? %>"><%= easy_icon part[:icon] %><%= part[:name]
12
+ %><% if part[:count] %><%= tag.span part[:count], class: class_names("n", "js-count": !part[:aside]) %><% end
13
+ %><% if part[:dot] %><span class="part-tabs-dot" title="<%= part[:dot] %>"></span><% end %></a>
14
+ <% end %>
15
+ <% end %>
16
+ </ul>
17
+ <% parts.each_with_index do |part, index| %>
18
+ <section id="<%= part[:id] %>" class="<%= class_names "part-tabs-panel", "is-off": index.positive? %>"
19
+ aria-labelledby="<%= part[:id] %>-h">
20
+ <h2 id="<%= part[:id] %>-h"><%= part[:name] %></h2>
21
+ <%= yield part %>
22
+ </section>
23
+ <% end %>
24
+ <%= javascript_include_tag "easy/tabs", defer: true %>
@@ -0,0 +1,10 @@
1
+ <%# locals: (name:, id: nil, autofocus: false, label: "6-digit code") -%>
2
+ <%# The 6-digit code emailed to sign in or to sign: one real field, so a pasted or autofilled code lands whole, drawn
3
+ over six slots by easy/pin_code.js, which the page includes once; without the script, the field itself shows.
4
+ name: the field's, "sign_in[pin]"; a code refused is the page's to mark, is-invalid on the .pin-code. -%>
5
+ <div class="pin-code">
6
+ <%= tag.input type: "text", name:, id:, class: "pin-code-input", required: true, autofocus:,
7
+ autocomplete: "one-time-code", inputmode: "numeric", pattern: "[0-9]{6}", maxlength: 6, aria: { label: } %>
8
+ <div class="pin-code-slots"
9
+ aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span></div>
10
+ </div>
@@ -0,0 +1,28 @@
1
+ <%# locals: (heading: nil, heading_id: nil, before: nil, switches_id: "switches", switches: [], after: nil, search: nil, search_label: search, sort_list: nil, sort_default: "newest") -%>
2
+ <%# The head of a report or list: its heading, its switches (each { state:, css:, icon:, count:, label:, off: }), with the
3
+ sort button at their end, and a search. before and after go above and below the switches. sort_list is the list, or the lists, the
4
+ button flips by each row's data-sort (easy/report_sort); without it the page sorts by itself. Inside js-switches
5
+ the switches and the search show or hide the rows by their data-state (easy/switches), and the words searched are
6
+ marked in the rows left (easy/highlight). -%>
7
+ <%= tag.section class: "report-head", data: { sort_list:, sort_default: (sort_default if sort_list) }.compact do %>
8
+ <% if heading %><%= tag.h2 heading, id: heading_id %><% end %>
9
+ <%= before %>
10
+ <% if switches.any? %>
11
+ <%= tag.ul class: "switches levels", id: switches_id do %>
12
+ <% switches.each do |item| %>
13
+ <%= tag.li class: item[:css], data: { state: item[:state], off: item[:off] }.compact do
14
+ %><% if item[:icon] %><%= easy_icon item[:icon] %><% end %><span class="n"><%= item[:count] %></span>
15
+ <%= item[:label] %><% end %>
16
+ <% end %>
17
+ <li class="sort-button-item" hidden><button type="button" class="sort-button js-sort"><%=
18
+ easy_icon sort_default %><span class="js-sort-label"><%= sort_default.capitalize %> first</span></button></li>
19
+ <% end %>
20
+ <% end %>
21
+ <%= after %>
22
+ <% if search %><label class="word-search report-search js-words" hidden><%= easy_icon "search" %><input
23
+ type="search" placeholder="<%= search %>" aria-label="<%= search_label %>" autocomplete="off"
24
+ spellcheck="false"></label><% end %>
25
+ <% end %>
26
+ <% if sort_list %><%= javascript_include_tag "easy/report_sort", defer: true %><% end %>
27
+ <% if search %><%= javascript_include_tag "easy/highlight", defer: true %><% end %>
28
+ <% if switches.any? || search %><%= javascript_include_tag "easy/switches", defer: true %><% end %>
@@ -0,0 +1,10 @@
1
+ <%# locals: (label:, href:, padding: "10px 28px 18px") -%>
2
+ <%# A row of an email's card (easy/mailer/card) holding its one button: the blue pill, centered. padding: the room
3
+ around it, for a row with nothing over or under it. -%>
4
+ <tr><td align="center" style="padding:<%= padding %>;">
5
+ <table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr>
6
+ <td style="background:#1a70d0;border-radius:24px;">
7
+ <a href="<%= href %>" style="display:inline-block;padding:14px 30px;<%= easy_mailer_font :heading %>font-size:17px;font-weight:600;color:#ffffff;text-decoration:none;border-radius:24px;"><%= label %></a>
8
+ </td>
9
+ </tr></table>
10
+ </td></tr>
@@ -0,0 +1,23 @@
1
+ <%# locals: (title: nil, label: nil, note: nil, code: false) -%>
2
+ <%# A card on an email's page (easy/mailer/frame): white and rounded, under a navy head that says what the email is
3
+ about when it has a title; the block draws its rows, each a <tr> (easy/mailer/text, easy/mailer/button). label:
4
+ the small words over the title, "A new AD may affect"; title: "N714AD"; note: a line under it; code: the title is
5
+ a code to type, drawn large, centered and selected whole by one click or tap. -%>
6
+ <tr><td style="background:#ffffff;border:1px solid #c6cbcf;border-radius:18px;overflow:hidden;">
7
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
8
+ <% if title && code %>
9
+ <tr><td align="center" style="background:#0b1e3a;padding:30px 28px 30px;border-radius:17px 17px 0 0;">
10
+ <div style="<%= easy_mailer_font %>font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:#9fb6d4;margin:0 0 12px;"><%= label %></div>
11
+ <div style="<%= easy_mailer_font :heading %>font-size:56px;font-weight:700;letter-spacing:.14em;color:#ffffff;line-height:1;-webkit-user-select:all;user-select:all;"><%= title %></div>
12
+ <% if note %><div style="<%= easy_mailer_font %>font-size:15px;line-height:1.4;color:#cfe0f3;margin:14px 0 0;"><%= note %></div><% end %>
13
+ </td></tr>
14
+ <% elsif title %>
15
+ <tr><td style="background:#0b1e3a;padding:26px 28px;border-radius:17px 17px 0 0;">
16
+ <% if label %><div style="<%= easy_mailer_font %>font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:#9fb6d4;margin:0 0 6px;"><%= label %></div><% end %>
17
+ <div style="<%= easy_mailer_font :heading %>font-size:38px;font-weight:700;letter-spacing:.02em;color:#ffffff;line-height:1.1;"><%= title %></div>
18
+ <% if note %><div style="<%= easy_mailer_font %>font-size:17px;color:#dfe5ff;margin-top:6px;"><%= note %></div><% end %>
19
+ </td></tr>
20
+ <% end %>
21
+ <%= yield %>
22
+ </table>
23
+ </td></tr>
@@ -0,0 +1,28 @@
1
+ <%# locals: (code:, brand:, site:, logo:, purpose:, lifetime:, ignore:, link: nil, link_label: nil, link_lede: nil, fine: nil) -%>
2
+ <%# The email carrying a 6-digit code, the same from every easy app (its text twin: _code.text.erb): the logo, then a
3
+ card whose navy head holds the code large enough to read across the room, selected whole by one click or tap.
4
+ The code is plain digits next to the words "verification code", in the subject too ("123456 is your easyFAA
5
+ verification code"), with no other run of digits near it: what Apple Mail, iOS and Gmail look for to offer it to
6
+ the field that asks for it.
7
+ code: "123456"; brand: "easyFAA"; site: the app's address, under the card; logo: the URL of the brand's logo,
8
+ an inline attachment so it shows wherever the email is read; purpose: "to sign in", "to sign the airframe
9
+ logbook entry for N22196"; lifetime: "10 minutes"; ignore: what to do with an email nobody asked for.
10
+ link, link_label, link_lede: a button doing what the code does, the sentence above it, which names the code again as
11
+ the email's own does without one ("Type 123456 in the window you left open."); fine: under it (HTML). -%>
12
+ <% page = { brand:, site:, logo:, title: "#{code} is your #{brand} verification code",
13
+ preview: "Your #{brand} verification code #{purpose}: #{code}" } %>
14
+ <%= render layout: "easy/mailer/frame", locals: page do %>
15
+ <%= render layout: "easy/mailer/card", locals: { title: code, label: "Your #{brand} verification code", note: purpose.upcase_first, code: true } do %>
16
+ <% if link %>
17
+ <tr><td align="center" style="padding:22px 28px 8px;<%= easy_mailer_font %>font-size:16px;line-height:1.5;color:#2c2f32;"><%= link_lede %></td></tr>
18
+ <%= render "easy/mailer/button", label: link_label, href: link %>
19
+ <% else %>
20
+ <%# The code again, in a plain sentence: Mail offers a code it reads in running words to the field that asks. -%>
21
+ <tr><td align="center" style="padding:22px 28px 8px;<%= easy_mailer_font %>font-size:16px;line-height:1.5;color:#2c2f32;">Type <%= code %> in the window you left open.</td></tr>
22
+ <% end %>
23
+ <tr><td align="center" style="padding:0 28px 26px;<%= easy_mailer_font %>font-size:13px;line-height:1.5;color:#636c74;">
24
+ <% if fine %><%= fine %><br><% end %>
25
+ It works once, for <%= lifetime %>. <%= ignore %>
26
+ </td></tr>
27
+ <% end %>
28
+ <% end %>
@@ -0,0 +1,19 @@
1
+ <%# locals: (code:, brand:, site:, purpose:, lifetime:, ignore:, logo: nil, link: nil, link_label: nil, link_lede: nil, fine: nil) -%>
2
+ <%# The plain-text twin of _code.html.erb: the code first, on its own line beside the words "verification code". fine: plain text. -%>
3
+ Your <%= brand %> verification code <%= purpose %>: <%= code %>
4
+ <% if link %>
5
+
6
+ <%= link_lede %>
7
+ <%= link %>
8
+ <% else %>
9
+
10
+ Type <%= code %> in the window you left open.
11
+ <% end %>
12
+ <% if fine %>
13
+
14
+ <%= fine %>
15
+ <% end %>
16
+
17
+ It works once, for <%= lifetime %>. <%= ignore %>
18
+
19
+ <%= site %>
@@ -0,0 +1,35 @@
1
+ <%# locals: (brand:, site:, logo:, title:, preview: nil, footer: nil) -%>
2
+ <%# The page every easy app's email is drawn on (render layout: "easy/mailer/frame"): the brand's logo over what the
3
+ block draws, cards (easy/mailer/card) and rows of its own, then the site's address. Styled inline and laid out in
4
+ tables, as email clients need. brand: "easyFAA"; site: the app's address; logo: the URL of the brand's logo, an
5
+ inline attachment so it shows wherever the email is read; title: the page's; preview: what an inbox shows after
6
+ the subject; footer: what the last row says in place of the site's address (HTML). -%>
7
+ <!DOCTYPE html>
8
+ <html lang="en">
9
+ <head>
10
+ <meta charset="utf-8">
11
+ <meta name="viewport" content="width=device-width, initial-scale=1">
12
+ <meta name="color-scheme" content="light">
13
+ <meta name="supported-color-schemes" content="light">
14
+ <meta name="format-detection" content="telephone=no, date=no, address=no">
15
+ <title><%= title %></title>
16
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@500;600;700&amp;display=swap" rel="stylesheet">
17
+ </head>
18
+ <body style="margin:0;padding:0;background:#f1f2f3;">
19
+ <% if preview %><div style="display:none;max-height:0;overflow:hidden;opacity:0;"><%= preview %></div><% end %>
20
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f1f2f3;">
21
+ <tr><td align="center" style="padding:32px 16px;">
22
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="max-width:560px;">
23
+ <tr><td style="padding:0 4px 20px;">
24
+ <a href="<%= site %>" style="text-decoration:none;"><img
25
+ src="<%= logo %>" height="32" alt="<%= brand %>" style="display:block;border:0;height:32px;width:auto;"></a>
26
+ </td></tr>
27
+ <%= yield %>
28
+ <tr><td style="padding:18px 8px 0;<%= easy_mailer_font %>font-size:12px;line-height:1.5;color:#636c74;text-align:center;">
29
+ <%= footer || link_to(URI(site).host, site, style: "color:#636c74;") %>
30
+ </td></tr>
31
+ </table>
32
+ </td></tr>
33
+ </table>
34
+ </body>
35
+ </html>
@@ -0,0 +1,7 @@
1
+ <%# locals: (small: false) -%>
2
+ <%# A row of words in an email's card (easy/mailer/card): the block's paragraphs, or its fine print (small). -%>
3
+ <% if small %>
4
+ <tr><td style="padding:0 28px 26px;<%= easy_mailer_font %>font-size:13px;line-height:1.5;color:#636c74;"><%= yield %></td></tr>
5
+ <% else %>
6
+ <tr><td style="padding:12px 28px 8px;<%= easy_mailer_font %>font-size:16px;line-height:1.55;color:#2c2f32;"><%= yield %></td></tr>
7
+ <% end %>
@@ -0,0 +1,17 @@
1
+ <% content_for :title, "#{@heading} | #{@site_name}" %>
2
+ <h1>
3
+ <svg viewBox="0 0 64 64" aria-hidden="true">
4
+ <g stroke="currentColor" stroke-width="2.5" stroke-linejoin="round">
5
+ <polygon points="60,8 4,30 25,37" fill="#ffffff"/>
6
+ <polygon points="60,8 25,37 31,57" fill="#cfe0f3"/>
7
+ </g>
8
+ <g stroke="#7a93b3" stroke-width="1.6" stroke-linecap="round">
9
+ <line x1="16" y1="28.5" x2="36" y2="21"/><line x1="21" y1="31.5" x2="44" y2="22.5"/>
10
+ <line x1="27" y1="33" x2="46" y2="25.5"/>
11
+ </g>
12
+ <path d="M10 44 C14 50 18 53 24 55" fill="none" stroke="currentColor" stroke-width="2" stroke-dasharray="3 3"
13
+ stroke-linecap="round"/>
14
+ </svg>
15
+ <%= @heading %>
16
+ </h1>
17
+ <p><%= @message %></p>
@@ -0,0 +1,55 @@
1
+ <%# The page every easy app draws: an app's own layout fills the slots, then renders this one
2
+ (render template: "layouts/easy/site").
3
+ - :easy_head, its title, metas and links;
4
+ - :easy_stylesheets, its own stylesheets, after the shared ones;
5
+ - :body_class, the body's classes;
6
+ - :site_brand_url and :site_brand, where its logo goes and the logo;
7
+ - :site_nav, its links, sign in or out;
8
+ - :sky_band, any word, for the band under the header; then the page is wide;
9
+ - :page_class, the page's own classes beside page ("wide" for one that lays out its own widths);
10
+ - :site_footer, its footer's lines and links;
11
+ - :body_end, its scripts. -%>
12
+ <!doctype html>
13
+ <html lang="en">
14
+ <head>
15
+ <meta charset="utf-8">
16
+ <meta name="viewport" content="width=device-width, initial-scale=1">
17
+ <%= yield :easy_head %>
18
+ <%# The headings' face, asked for with the stylesheet rather than once it is read (easy/base.css). -%>
19
+ <%= preload_link_tag asset_path("easy/outfit-latin.woff2"), as: "font", type: "font/woff2", crossorigin: "anonymous" %>
20
+ <%= stylesheet_link_tag "easy/tokens", "easy/base", "easy/layout", "easy/components" %>
21
+ <%= yield :easy_stylesheets %>
22
+ <script>(function(){document.documentElement.className+=' has-js';try{var t=localStorage.getItem('theme');
23
+ if(t==='dark'||t==='light'){document.documentElement.setAttribute('data-theme',t)}}catch(e){}})();</script>
24
+ </head>
25
+ <body class="<%= content_for(:body_class) %>">
26
+ <a class="skip-link" href="#main">Skip to content</a>
27
+ <header class="site-header">
28
+ <div class="page-width">
29
+ <%= link_to content_for(:site_brand_url) || "/", class: "site-brand" do %><%= yield :site_brand %><% end %>
30
+ <div>
31
+ <nav class="site-nav" aria-label="Site"><%= yield :site_nav %></nav>
32
+ <button type="button" class="theme-toggle js-theme" aria-pressed="false">
33
+ <%= easy_icon "sun", class: "i i-sun" %><%= easy_icon "moon", class: "i i-moon" %><span
34
+ class="theme-toggle-label">Dark theme</span>
35
+ </button>
36
+ </div>
37
+ </div>
38
+ </header>
39
+ <% if content_for?(:sky_band) %>
40
+ <div class="sky-band"></div>
41
+ <% end %>
42
+ <main id="main"
43
+ class="page<%= " wide" if content_for?(:sky_band) %><%= " #{content_for(:page_class)}" if content_for?(:page_class) %>">
44
+ <%= yield %>
45
+ </main>
46
+ <footer id="footer" class="site-footer">
47
+ <div class="site-footer-inner">
48
+ <%= yield :site_footer %>
49
+ </div>
50
+ </footer>
51
+ <%# The header's theme button, and any theme radios a page draws. -%>
52
+ <%= javascript_include_tag "easy/theme", defer: true %>
53
+ <%= yield :body_end %>
54
+ </body>
55
+ </html>
@@ -0,0 +1,30 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta name="color-scheme" content="light dark">
7
+ <meta name="robots" content="noindex">
8
+ <title><%= content_for(:title) || @site_name %></title>
9
+ <style>
10
+ :root{color-scheme:light;--bg:#ffffff;--ink:#15202e;--muted:#50575d;--primary:#1a70d0}
11
+ @media (prefers-color-scheme: dark){
12
+ :root{color-scheme:dark;--bg:#131415;--ink:#f3f5f8;--muted:#abb2b8;--primary:#689dff}
13
+ }
14
+ *{box-sizing:border-box}
15
+ body{margin:0;min-height:100svh;display:flex;flex-direction:column;align-items:center;justify-content:center;
16
+ background:var(--bg);color:var(--ink);
17
+ font:500 17px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;text-align:center;padding:40px 24px}
18
+ a{color:var(--primary)}
19
+ h1{display:flex;align-items:center;justify-content:center;font-size:clamp(1.5rem,5.5vw,2.25rem);margin:0}
20
+ h1 svg{width:1.1em;height:1.1em;margin-right:0.3em;flex-shrink:0}
21
+ p{color:var(--muted);margin:18px 0 0;max-width:32rem}
22
+ footer{margin-top:40px;font-size:15px;color:var(--muted)}
23
+ footer a{color:inherit;text-decoration:none}
24
+ </style>
25
+ </head>
26
+ <body>
27
+ <%= yield %>
28
+ <footer><%= link_to @site_name, @site_url %></footer>
29
+ </body>
30
+ </html>
@@ -0,0 +1,26 @@
1
+ # The single-engine airplane families easyFAA tells apart by make and model ("Cessna 172N",
2
+ # "Piper PA-18-150", "Van's RV-8"), tried in this order, the first pattern found in the name
3
+ # winning. A name no pattern finds is drawn high-wing, as the commonest. Case is ignored.
4
+ fixed-wing-drone:
5
+ - v-?bat|zipline|\buav\b|\buas\b|unmanned|drone
6
+ biplane:
7
+ - stearman|\bpt-?1[37]\b|\bn2s\b|pitts|\bs-?[12][abcs]?\b|waco|great lakes|starduster|skybolt|acro ?sport
8
+ - hatz|miniplane|christen|eagle ii|\bultimate\b|tiger moth|\bjungmann
9
+ ag-plane:
10
+ - air tractor|\bat-?[3-8]\d\d|thrush|\bs-?2r|ag-?cat|g-?164|\bpa-?25|pawnee|\bpa-?36|ag ?(wagon|truck|husky)|\b188\b
11
+ floatplane:
12
+ - \blake\b|\bla-?(4|250)|searey|sea rey|icon a5|seawind|\bg-?(21|44|73)\b|goose|widgeon|seabee|rc-?3\b|skimmer|catalina
13
+ low-wing-tailwheel:
14
+ - \brv-?[3468]\b(?!a)|\bat-?6\b|\bt-?6\b|\bsnj\b|harvard|\bp-?51\b|mustang|\bpt-?(19|22|26)\b|\bextra\b|\bea-?300
15
+ - edge 540|\bgiles\b|\blaser\b|\bcap ?(10|21|23)|\bsu-?(26|29|31)\b|\bt-?28\b
16
+ high-wing-tailwheel:
17
+ - \bj-?[2345]\b|\bpa-?(11|12|14|16|17|18|20)\b|cub|aeronca|champ|citabria|decathlon|scout|\b7[acegk]c|\b8kcab
18
+ - taylorcraft|\bbc-?12|cessna (a)?1(20|40|70|80|85|95)|\b(a)?185|maule|husky|\baviat\b|stinson|luscombe|kitfox|savage
19
+ - \bm-?[4-7]\b|helio|courier|\bdhc-?2\b|beaver|\bl-?(2|3|4|16|19)\b|bird ?dog|super ?cub|carbon ?cub|backcountry
20
+ low-wing-retractable:
21
+ - mooney|\bm20|(beech|raytheon|textron|hawker).*\b[a-v]?(33|35|36)[a-z]?\b|bonanza|debonair|\bpa-?(24|28r|30|32r|39|46)\b|comanche|arrow|saratoga
22
+ - lance|\bt-?34\b|meridian|malibu|mirage|navion|\bm-?18|\bb-?24r|sierra|swift|\bgc-?1|viking|\b17-?3\d|bellanca 14
23
+ low-wing:
24
+ - \bpa-?(28|32|38)\b|cherokee|warrior|archer|cadet|tomahawk|cirrus|\bsr-?2[02]|\bda-?(20|40)|diamond|katana
25
+ - \baa-?(1|5)|grumman|tiger|cheetah|traveler|musketeer|sundowner|\bbe-?23|\bc-?23\b|\brv-?\d+|ercoupe|aircoupe|\balon\b
26
+ - lancair|glasair|sonex|zenith|\bch-?[67]\d\d|tecnam|evektor|sportstar|\bsr-?20