neon_sakura 0.1.4

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 (251) hide show
  1. checksums.yaml +7 -0
  2. data/.ai-reviewer/README.md +182 -0
  3. data/.ai-reviewer/ai-reviewer.sh +56 -0
  4. data/.ai-reviewer/build-system-prompt.sh +136 -0
  5. data/.ai-reviewer/extract-claude-sections.sh +32 -0
  6. data/.ai-reviewer/test-ai-reviewer.sh +40 -0
  7. data/.ai-reviewer-config.yml +190 -0
  8. data/.github/dependabot.yml +12 -0
  9. data/.github/settings.yml +70 -0
  10. data/.github/workflows/ai-pr-review-on-comment.yml +384 -0
  11. data/.github/workflows/ai-pr-review.yml +328 -0
  12. data/.github/workflows/license-check.yml +78 -0
  13. data/.github/workflows/lint.yml +79 -0
  14. data/.github/workflows/security.yml +131 -0
  15. data/.github/workflows/semgrep.yml +26 -0
  16. data/.github/workflows/test.yml +44 -0
  17. data/.gitignore +75 -0
  18. data/.rubocop.yml +33 -0
  19. data/.ruby-version +1 -0
  20. data/.simplecov +14 -0
  21. data/.stylelintignore +10 -0
  22. data/.stylelintrc.json +37 -0
  23. data/AGENTS.md +51 -0
  24. data/CHANGELOG.md +568 -0
  25. data/CLAUDE.md +632 -0
  26. data/Gemfile +8 -0
  27. data/Gemfile.lock +327 -0
  28. data/LICENSE +21 -0
  29. data/README.md +1209 -0
  30. data/Rakefile +25 -0
  31. data/app/assets/images/cherry_blossom.svg +1525 -0
  32. data/app/assets/images/cherry_blossom_tree.png +0 -0
  33. data/app/assets/images/prysm-icon.png +0 -0
  34. data/app/assets/stylesheets/base.css +29 -0
  35. data/app/assets/stylesheets/components.css +1652 -0
  36. data/app/assets/stylesheets/forms.css +152 -0
  37. data/app/assets/stylesheets/loading.css +145 -0
  38. data/app/assets/stylesheets/neon_sakura.css +40 -0
  39. data/app/assets/stylesheets/pagy-tailwind.css +120 -0
  40. data/app/assets/stylesheets/theme-default.css +40 -0
  41. data/app/assets/stylesheets/theme-green.css +84 -0
  42. data/app/assets/stylesheets/theme-purple.css +94 -0
  43. data/app/assets/stylesheets/theme-red.css +84 -0
  44. data/app/assets/stylesheets/utility-borders.css +29 -0
  45. data/app/assets/stylesheets/utility-colors.css +185 -0
  46. data/app/assets/stylesheets/utility-effects.css +123 -0
  47. data/app/assets/stylesheets/utility-gradients.css +158 -0
  48. data/app/assets/stylesheets/utility-layout.css +132 -0
  49. data/app/assets/stylesheets/utility-reset.css +13 -0
  50. data/app/assets/stylesheets/utility-responsive.css +145 -0
  51. data/app/assets/stylesheets/utility-sizing.css +99 -0
  52. data/app/assets/stylesheets/utility-spacing.css +174 -0
  53. data/app/assets/stylesheets/utility-typography.css +97 -0
  54. data/app/controllers/errors_controller.rb +120 -0
  55. data/app/controllers/style_guide_controller.rb +117 -0
  56. data/app/helpers/errors_helper.rb +12 -0
  57. data/app/helpers/neon_sakura/navbar_helper.rb +43 -0
  58. data/app/helpers/style_guide_helper.rb +36 -0
  59. data/app/javascript/neon_sakura/dropdown.js +22 -0
  60. data/app/javascript/neon_sakura/navbar.js +71 -0
  61. data/app/javascript/neon_sakura/theme_switcher.js +187 -0
  62. data/app/views/errors/show.html.erb +105 -0
  63. data/app/views/layouts/error.html.erb +19 -0
  64. data/app/views/layouts/mission_control/jobs/_application_selection.html.erb +14 -0
  65. data/app/views/layouts/mission_control/jobs/_navigation.html.erb +21 -0
  66. data/app/views/layouts/mission_control/jobs/application.html.erb +453 -0
  67. data/app/views/layouts/style_guide.html.erb +416 -0
  68. data/app/views/shared/_file_upload.html.erb +184 -0
  69. data/app/views/shared/_footer.html.erb +23 -0
  70. data/app/views/shared/_header.html.erb +42 -0
  71. data/app/views/shared/_navbar.html.erb +306 -0
  72. data/app/views/shared/_profile_image_selector.html.erb +165 -0
  73. data/app/views/shared/_theme_switcher.html.erb +64 -0
  74. data/app/views/shared/icons/_adjustments.html.erb +10 -0
  75. data/app/views/shared/icons/_alert_circle.html.erb +3 -0
  76. data/app/views/shared/icons/_alert_triangle.html.erb +3 -0
  77. data/app/views/shared/icons/_archive.html.erb +3 -0
  78. data/app/views/shared/icons/_arrow_down.html.erb +3 -0
  79. data/app/views/shared/icons/_arrow_left.html.erb +3 -0
  80. data/app/views/shared/icons/_arrow_up.html.erb +3 -0
  81. data/app/views/shared/icons/_arrows_pointing_in.html.erb +10 -0
  82. data/app/views/shared/icons/_arrows_pointing_out.html.erb +10 -0
  83. data/app/views/shared/icons/_artemis_logo.html.erb +26 -0
  84. data/app/views/shared/icons/_auth_banner.html.erb +1 -0
  85. data/app/views/shared/icons/_bars.html.erb +10 -0
  86. data/app/views/shared/icons/_bell.html.erb +3 -0
  87. data/app/views/shared/icons/_book.html.erb +3 -0
  88. data/app/views/shared/icons/_bookmark.html.erb +3 -0
  89. data/app/views/shared/icons/_box.html.erb +3 -0
  90. data/app/views/shared/icons/_brain.html.erb +3 -0
  91. data/app/views/shared/icons/_briefcase.html.erb +3 -0
  92. data/app/views/shared/icons/_calendar.html.erb +3 -0
  93. data/app/views/shared/icons/_camera.html.erb +4 -0
  94. data/app/views/shared/icons/_chart_bar.html.erb +3 -0
  95. data/app/views/shared/icons/_chart_line.html.erb +10 -0
  96. data/app/views/shared/icons/_chart_pie.html.erb +11 -0
  97. data/app/views/shared/icons/_chat.html.erb +3 -0
  98. data/app/views/shared/icons/_check.html.erb +3 -0
  99. data/app/views/shared/icons/_check_circle.html.erb +3 -0
  100. data/app/views/shared/icons/_cherry_blossom.html.erb +1516 -0
  101. data/app/views/shared/icons/_cherry_blossom_silhouette.html.erb +1016 -0
  102. data/app/views/shared/icons/_cherry_blossom_single_flower.html.erb +1125 -0
  103. data/app/views/shared/icons/_cherry_blossom_tree.html.erb +159 -0
  104. data/app/views/shared/icons/_chevron_down.html.erb +3 -0
  105. data/app/views/shared/icons/_chevron_right.html.erb +9 -0
  106. data/app/views/shared/icons/_clipboard.html.erb +3 -0
  107. data/app/views/shared/icons/_clock.html.erb +3 -0
  108. data/app/views/shared/icons/_close.html.erb +3 -0
  109. data/app/views/shared/icons/_cog.html.erb +4 -0
  110. data/app/views/shared/icons/_crop.html.erb +10 -0
  111. data/app/views/shared/icons/_crown.html.erb +3 -0
  112. data/app/views/shared/icons/_disc.html.erb +3 -0
  113. data/app/views/shared/icons/_download.html.erb +3 -0
  114. data/app/views/shared/icons/_dragonfly.html.erb +58 -0
  115. data/app/views/shared/icons/_duplicate.html.erb +4 -0
  116. data/app/views/shared/icons/_edit.html.erb +3 -0
  117. data/app/views/shared/icons/_envelope.html.erb +3 -0
  118. data/app/views/shared/icons/_eraser.html.erb +10 -0
  119. data/app/views/shared/icons/_external_link.html.erb +3 -0
  120. data/app/views/shared/icons/_eye.html.erb +4 -0
  121. data/app/views/shared/icons/_file_csv.html.erb +10 -0
  122. data/app/views/shared/icons/_file_export.html.erb +10 -0
  123. data/app/views/shared/icons/_file_image.html.erb +10 -0
  124. data/app/views/shared/icons/_file_import.html.erb +10 -0
  125. data/app/views/shared/icons/_file_question.html.erb +6 -0
  126. data/app/views/shared/icons/_film.html.erb +3 -0
  127. data/app/views/shared/icons/_filter.html.erb +3 -0
  128. data/app/views/shared/icons/_folder.html.erb +3 -0
  129. data/app/views/shared/icons/_folder_open.html.erb +3 -0
  130. data/app/views/shared/icons/_folder_plus.html.erb +3 -0
  131. data/app/views/shared/icons/_globe.html.erb +3 -0
  132. data/app/views/shared/icons/_google.html.erb +11 -0
  133. data/app/views/shared/icons/_heart.html.erb +3 -0
  134. data/app/views/shared/icons/_heart_broken.html.erb +11 -0
  135. data/app/views/shared/icons/_heart_pulse.html.erb +4 -0
  136. data/app/views/shared/icons/_history.html.erb +11 -0
  137. data/app/views/shared/icons/_home.html.erb +10 -0
  138. data/app/views/shared/icons/_image.html.erb +3 -0
  139. data/app/views/shared/icons/_inbox.html.erb +3 -0
  140. data/app/views/shared/icons/_info_circle.html.erb +10 -0
  141. data/app/views/shared/icons/_key.html.erb +3 -0
  142. data/app/views/shared/icons/_layers.html.erb +10 -0
  143. data/app/views/shared/icons/_lightbulb.html.erb +10 -0
  144. data/app/views/shared/icons/_lightning.html.erb +3 -0
  145. data/app/views/shared/icons/_list.html.erb +3 -0
  146. data/app/views/shared/icons/_lock.html.erb +3 -0
  147. data/app/views/shared/icons/_logout.html.erb +3 -0
  148. data/app/views/shared/icons/_magazine.html.erb +3 -0
  149. data/app/views/shared/icons/_magic.html.erb +3 -0
  150. data/app/views/shared/icons/_minus.html.erb +10 -0
  151. data/app/views/shared/icons/_mobile.html.erb +10 -0
  152. data/app/views/shared/icons/_moon.html.erb +3 -0
  153. data/app/views/shared/icons/_network.html.erb +10 -0
  154. data/app/views/shared/icons/_new_item_banner.html.erb +1 -0
  155. data/app/views/shared/icons/_ouroboros.html.erb +24 -0
  156. data/app/views/shared/icons/_package.html.erb +3 -0
  157. data/app/views/shared/icons/_palette.html.erb +3 -0
  158. data/app/views/shared/icons/_paper_plane.html.erb +10 -0
  159. data/app/views/shared/icons/_photo.html.erb +10 -0
  160. data/app/views/shared/icons/_play.html.erb +4 -0
  161. data/app/views/shared/icons/_plus.html.erb +3 -0
  162. data/app/views/shared/icons/_pocket.html.erb +11 -0
  163. data/app/views/shared/icons/_prysm-icon.html.erb +34 -0
  164. data/app/views/shared/icons/_prysm.html.erb +13 -0
  165. data/app/views/shared/icons/_pushbullet-1.html.erb +29 -0
  166. data/app/views/shared/icons/_pushbullet-2.html.erb +2 -0
  167. data/app/views/shared/icons/_puzzle.html.erb +10 -0
  168. data/app/views/shared/icons/_qrcode.html.erb +3 -0
  169. data/app/views/shared/icons/_question.html.erb +3 -0
  170. data/app/views/shared/icons/_receipt.html.erb +10 -0
  171. data/app/views/shared/icons/_redo.html.erb +3 -0
  172. data/app/views/shared/icons/_refresh.html.erb +3 -0
  173. data/app/views/shared/icons/_rocket.html.erb +10 -0
  174. data/app/views/shared/icons/_rss.html.erb +3 -0
  175. data/app/views/shared/icons/_save.html.erb +3 -0
  176. data/app/views/shared/icons/_search.html.erb +3 -0
  177. data/app/views/shared/icons/_search_minus.html.erb +10 -0
  178. data/app/views/shared/icons/_search_plus.html.erb +10 -0
  179. data/app/views/shared/icons/_server_error.html.erb +6 -0
  180. data/app/views/shared/icons/_share.html.erb +3 -0
  181. data/app/views/shared/icons/_shield_check.html.erb +3 -0
  182. data/app/views/shared/icons/_sign_in.html.erb +3 -0
  183. data/app/views/shared/icons/_spinner.html.erb +4 -0
  184. data/app/views/shared/icons/_star.html.erb +3 -0
  185. data/app/views/shared/icons/_store.html.erb +10 -0
  186. data/app/views/shared/icons/_sun.html.erb +3 -0
  187. data/app/views/shared/icons/_sync.html.erb +3 -0
  188. data/app/views/shared/icons/_table.html.erb +3 -0
  189. data/app/views/shared/icons/_tag.html.erb +3 -0
  190. data/app/views/shared/icons/_tags.html.erb +11 -0
  191. data/app/views/shared/icons/_tools.html.erb +4 -0
  192. data/app/views/shared/icons/_trash.html.erb +3 -0
  193. data/app/views/shared/icons/_undo.html.erb +3 -0
  194. data/app/views/shared/icons/_unlock.html.erb +3 -0
  195. data/app/views/shared/icons/_upload.html.erb +3 -0
  196. data/app/views/shared/icons/_user.html.erb +3 -0
  197. data/app/views/shared/icons/_user_circle.html.erb +10 -0
  198. data/app/views/shared/icons/_user_plus.html.erb +10 -0
  199. data/app/views/shared/icons/_video.html.erb +3 -0
  200. data/app/views/shared/icons/_wrench.html.erb +11 -0
  201. data/app/views/style_guide/index.html.erb +77 -0
  202. data/app/views/style_guide/sections/_alerts.html.erb +114 -0
  203. data/app/views/style_guide/sections/_badges.html.erb +78 -0
  204. data/app/views/style_guide/sections/_buttons.html.erb +130 -0
  205. data/app/views/style_guide/sections/_cards.html.erb +84 -0
  206. data/app/views/style_guide/sections/_colors.html.erb +106 -0
  207. data/app/views/style_guide/sections/_file_upload.html.erb +135 -0
  208. data/app/views/style_guide/sections/_forms.html.erb +129 -0
  209. data/app/views/style_guide/sections/_gradients.html.erb +253 -0
  210. data/app/views/style_guide/sections/_header.html.erb +12 -0
  211. data/app/views/style_guide/sections/_icons.html.erb +55 -0
  212. data/app/views/style_guide/sections/_images.html.erb +40 -0
  213. data/app/views/style_guide/sections/_loading.html.erb +242 -0
  214. data/app/views/style_guide/sections/_pagination.html.erb +212 -0
  215. data/app/views/style_guide/sections/_profile_components.html.erb +203 -0
  216. data/app/views/style_guide/sections/_theme_switcher.html.erb +72 -0
  217. data/app/views/style_guide/sections/_typography.html.erb +65 -0
  218. data/bin/ai-optimize-claude-md +540 -0
  219. data/bin/ai-review-local +345 -0
  220. data/bin/ai-security-review +585 -0
  221. data/bin/brakeman +9 -0
  222. data/bin/install-hooks +57 -0
  223. data/bin/rake +7 -0
  224. data/bin/rubocop +10 -0
  225. data/bin/verify_setup.rb +31 -0
  226. data/config/brakeman.ignore +28 -0
  227. data/config/initializers/neon_sakura.rb +15 -0
  228. data/config/license_overrides.yml +13 -0
  229. data/config/routes.rb +21 -0
  230. data/config/theme_mappings.yml +61 -0
  231. data/docs/PRYSM_ASSETS.md +210 -0
  232. data/docs/plans/extract_ai_reviewer_plan.md +151 -0
  233. data/docs/plans/neon_sakura_gem_plan.md +138 -0
  234. data/lib/neon_sakura/configuration.rb +94 -0
  235. data/lib/neon_sakura/engine.rb +48 -0
  236. data/lib/neon_sakura/icon_helper.rb +54 -0
  237. data/lib/neon_sakura/profile_helper.rb +24 -0
  238. data/lib/neon_sakura/stylesheet_helper.rb +40 -0
  239. data/lib/neon_sakura/theme_helper.rb +63 -0
  240. data/lib/neon_sakura/theme_importer.rb +112 -0
  241. data/lib/neon_sakura/version.rb +5 -0
  242. data/lib/neon_sakura.rb +13 -0
  243. data/neon_sakura.gemspec +50 -0
  244. data/package.json +18 -0
  245. data/scripts/git-hooks/post-merge +132 -0
  246. data/scripts/git-hooks/pre-commit +123 -0
  247. data/scripts/git-hooks/pre-push +127 -0
  248. data/scripts/license-check.rb +587 -0
  249. data/settings.local.json +12 -0
  250. data/yarn.lock +778 -0
  251. metadata +503 -0
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Controller for the style guide - development/test only
4
+ # Displays all UI components, icons, themes, and code examples
5
+ class StyleGuideController < ActionController::Base
6
+ # Include Pagy method for pagination support (if available)
7
+ # Pagy 43+ uses Pagy::Method instead of Pagy::Backend
8
+ include Pagy::Method if defined?(Pagy::Method)
9
+ include Pagy::Backend if defined?(Pagy::Backend) && !defined?(Pagy::Method)
10
+
11
+ # Include helpers in controller for use in actions
12
+ include NeonSakura::IconHelper
13
+ include NeonSakura::ThemeHelper
14
+
15
+ # Make helpers available to views
16
+ helper Pagy::Frontend if defined?(Pagy::Frontend)
17
+ helper NeonSakura::IconHelper
18
+ helper NeonSakura::ThemeHelper
19
+ helper NeonSakura::StylesheetHelper
20
+
21
+ # Use null_session for CSRF - appropriate for read-only pages with no forms
22
+ # This resets the session instead of raising an exception for missing/invalid tokens
23
+ protect_from_forgery with: :exception
24
+
25
+ layout "style_guide"
26
+
27
+ # Before actions to enforce environment restriction
28
+ before_action :ensure_dev_or_test_environment
29
+ before_action :ensure_style_guide_enabled
30
+
31
+ def index
32
+ @page_title = "Neon Sakura Style Guide"
33
+
34
+ # Collect all icons dynamically
35
+ @icons = available_icons
36
+
37
+ # Collect all images dynamically
38
+ @images = available_images
39
+
40
+ # Set up Pagy pagination examples if enabled
41
+ @pagy_enabled = NeonSakura.configuration.style_guide_pagy_examples
42
+ @pagy_available = false
43
+ @pagy_error = nil
44
+
45
+ if @pagy_enabled
46
+ # Generate dummy data for Pagy pagination examples
47
+ @pagy_items = generate_pagy_dummy_data
48
+
49
+ # Try to set up Pagy if available
50
+ if defined?(Pagy)
51
+ begin
52
+ # Pagy 43+ uses pagy(:offset, collection, **options)
53
+ # Older Pagy versions use pagy_array(collection, **options)
54
+ if defined?(Pagy::Method)
55
+ # Pagy 43+ API
56
+ @pagy, @page_items = pagy(:offset, @pagy_items, limit: 10)
57
+ elsif respond_to?(:pagy_array)
58
+ # Older Pagy API with array extra
59
+ @pagy, @page_items = pagy_array(@pagy_items, limit: 10)
60
+ else
61
+ raise NoMethodError, "No compatible Pagy pagination method found"
62
+ end
63
+ @pagy_available = true
64
+ rescue StandardError => e
65
+ # Pagy initialization failed - provide helpful error message
66
+ @pagy_error = "Pagy pagination examples failed to initialize. " \
67
+ "Error: #{e.message}. " \
68
+ "To disable these examples, set config.style_guide_pagy_examples = false in your " \
69
+ "NeonSakura configuration."
70
+ end
71
+ else
72
+ # Pagy not installed
73
+ @pagy_error = "Pagy gem is not installed. Add 'gem \"pagy\"' to your Gemfile, " \
74
+ "or disable Pagy examples by setting config.style_guide_pagy_examples = false"
75
+ end
76
+ end
77
+
78
+ # Theme information
79
+ @available_themes = NeonSakura.config.available_themes
80
+ @current_theme = current_theme
81
+ end
82
+
83
+ private
84
+
85
+ def ensure_dev_or_test_environment
86
+ unless Rails.env.development? || Rails.env.test?
87
+ render plain: "Style guide only available in development and test environments", status: :not_found
88
+ end
89
+ end
90
+
91
+ def ensure_style_guide_enabled
92
+ unless NeonSakura.configuration.enable_style_guide
93
+ render plain: "Style guide is disabled", status: :not_found
94
+ end
95
+ end
96
+
97
+ # Get list of available images
98
+ def available_images
99
+ image_dir = File.join(NeonSakura::Engine.root, "app", "assets", "images")
100
+ return [] unless Dir.exist?(image_dir)
101
+
102
+ Dir.glob(File.join(image_dir, "*.{png,jpg,jpeg,svg,gif}")).map do |file|
103
+ File.basename(file)
104
+ end.sort
105
+ end
106
+
107
+ # Generate dummy data for Pagy examples
108
+ def generate_pagy_dummy_data
109
+ (1..100).map do |i|
110
+ {
111
+ id: i,
112
+ title: "Item #{i}",
113
+ description: "This is a sample item for pagination demo"
114
+ }
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Helper methods for error pages
4
+ module ErrorsHelper
5
+ # Include all route helpers for error pages
6
+ include Rails.application.routes.url_helpers
7
+
8
+ # Provide default_url_options for url_helpers
9
+ def default_url_options
10
+ Rails.application.config.action_mailer.default_url_options || {}
11
+ end
12
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NeonSakura
4
+ module NavbarHelper
5
+ # Check if a navigation link is currently active
6
+ # @param link [Hash] The link configuration
7
+ # @return [Boolean] true if the link is active
8
+ def nav_link_active?(link)
9
+ return false unless link[:path]
10
+
11
+ is_active = current_page?(link[:path]) rescue false
12
+ return true if is_active
13
+
14
+ # Check additional active_paths if present
15
+ if link[:active_paths]
16
+ link[:active_paths].any? { |path| current_page?(path) rescue false }
17
+ else
18
+ false
19
+ end
20
+ end
21
+
22
+ # Safely evaluate a condition string in the current context
23
+ # @param condition [String] The Ruby code to evaluate
24
+ # @return [Boolean] The result of the evaluation
25
+ def eval_nav_condition(condition)
26
+ return true if condition.nil? || condition.empty?
27
+
28
+ instance_eval(condition.to_s)
29
+ rescue StandardError => e
30
+ Rails.logger.warn("Failed to evaluate navbar condition '#{condition}': #{e.message}")
31
+ false
32
+ end
33
+
34
+ # Check if an icon exists before rendering
35
+ # @param icon_name [String] The icon partial name
36
+ # @return [Boolean] true if the icon partial exists
37
+ def nav_icon_exists?(icon_name)
38
+ return false if icon_name.blank?
39
+
40
+ lookup_context.exists?("shared/icons/#{icon_name}", [], true)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Helper methods for the style guide
4
+ module StyleGuideHelper
5
+ # Render a component example with preview and code
6
+ # @param title [String] The example title
7
+ # @param code [String] The ERB code to display
8
+ # @param options [Hash] Additional options
9
+ # @yield Block containing the component to render
10
+ # @return [String] HTML for the example with code
11
+ def example_with_code(title, code = nil, options = {}, &block)
12
+ content_tag :div, class: "code-example-wrapper #{options[:wrapper_class]}" do
13
+ preview_section(title, &block) + code_section(code)
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def preview_section(title, &block)
20
+ content_tag :div, class: "code-example-preview" do
21
+ title_tag = content_tag :h3, title, class: "text-xl font-semibold mb-4"
22
+ preview_content = capture(&block)
23
+ title_tag + preview_content
24
+ end
25
+ end
26
+
27
+ def code_section(code)
28
+ return "".html_safe if code.blank?
29
+
30
+ content_tag :div, class: "code-example-code" do
31
+ content_tag :pre do
32
+ content_tag :code, code
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,22 @@
1
+ // Dropdown functionality for Mission Control
2
+ document.addEventListener('DOMContentLoaded', function() {
3
+ // This function handles dropdown toggle behavior
4
+ const dropdowns = document.querySelectorAll('.dropdown-toggle');
5
+
6
+ dropdowns.forEach(function(dropdown) {
7
+ dropdown.addEventListener('click', function(e) {
8
+ e.preventDefault();
9
+ const parent = this.parentElement;
10
+ parent.classList.toggle('open');
11
+ });
12
+ });
13
+
14
+ // Close dropdowns when clicking outside
15
+ document.addEventListener('click', function(e) {
16
+ if (!e.target.closest('.dropdown')) {
17
+ document.querySelectorAll('.dropdown.open').forEach(function(dropdown) {
18
+ dropdown.classList.remove('open');
19
+ });
20
+ }
21
+ });
22
+ });
@@ -0,0 +1,71 @@
1
+ // Navbar dropdown functionality
2
+ // Converted from Stimulus controller to vanilla JavaScript for broader compatibility
3
+
4
+ document.addEventListener('DOMContentLoaded', function() {
5
+ initializeNavbarDropdowns();
6
+ });
7
+
8
+ // Also initialize on Turbo page loads if Turbo is present
9
+ if (typeof Turbo !== 'undefined') {
10
+ document.addEventListener('turbo:load', function() {
11
+ initializeNavbarDropdowns();
12
+ });
13
+ }
14
+
15
+ function initializeNavbarDropdowns() {
16
+ const dropdowns = document.querySelectorAll('[data-navbar-dropdown]');
17
+
18
+ dropdowns.forEach(function(dropdown) {
19
+ const toggle = dropdown.querySelector('[data-action="toggle"]');
20
+ const menu = dropdown.querySelector('[data-menu]');
21
+
22
+ if (!toggle || !menu) return;
23
+
24
+ // Remove any existing listeners to prevent duplicates
25
+ const newToggle = toggle.cloneNode(true);
26
+ toggle.parentNode.replaceChild(newToggle, toggle);
27
+
28
+ // Add click handler for toggle button
29
+ newToggle.addEventListener('click', function(event) {
30
+ event.preventDefault();
31
+ event.stopPropagation();
32
+
33
+ const isOpen = dropdown.classList.contains('open');
34
+
35
+ // Close all other dropdowns first
36
+ closeAllNavDropdowns();
37
+
38
+ // Toggle this dropdown
39
+ if (!isOpen) {
40
+ dropdown.classList.add('open');
41
+ newToggle.setAttribute('aria-expanded', 'true');
42
+ }
43
+ });
44
+ });
45
+
46
+ // Close dropdowns when clicking outside
47
+ document.addEventListener('click', function(event) {
48
+ if (!event.target.closest('[data-navbar-dropdown]')) {
49
+ closeAllNavDropdowns();
50
+ }
51
+ });
52
+
53
+ // Close dropdowns on Escape key
54
+ document.addEventListener('keydown', function(event) {
55
+ if (event.key === 'Escape') {
56
+ closeAllNavDropdowns();
57
+ }
58
+ });
59
+ }
60
+
61
+ function closeAllNavDropdowns() {
62
+ const openDropdowns = document.querySelectorAll('[data-navbar-dropdown].open');
63
+
64
+ openDropdowns.forEach(function(dropdown) {
65
+ dropdown.classList.remove('open');
66
+ const toggle = dropdown.querySelector('[data-action="toggle"]');
67
+ if (toggle) {
68
+ toggle.setAttribute('aria-expanded', 'false');
69
+ }
70
+ });
71
+ }
@@ -0,0 +1,187 @@
1
+ // Theme switcher functionality for neon_sakura
2
+ // Handles theme switching via localStorage with optional backend persistence
3
+
4
+ (function() {
5
+ 'use strict';
6
+
7
+ // Only initialize once
8
+ if (window.neonSakuraThemeInitialized) return;
9
+ window.neonSakuraThemeInitialized = true;
10
+
11
+ const STORAGE_KEY_NAME = 'neon_sakura_theme_name';
12
+ const STORAGE_KEY_MODE = 'neon_sakura_theme_mode';
13
+
14
+ // Get available themes from configuration (embedded in page)
15
+ function getAvailableThemes() {
16
+ const themesElement = document.querySelector('[data-available-themes]');
17
+ if (themesElement) {
18
+ try {
19
+ return JSON.parse(themesElement.dataset.availableThemes);
20
+ } catch (e) {
21
+ console.error('Failed to parse available themes:', e);
22
+ }
23
+ }
24
+ // Fallback to single default theme
25
+ return [{ name: 'purple', mode: 'dark', label: 'Purple Dark' }];
26
+ }
27
+
28
+ // Get current theme from localStorage or default
29
+ function getCurrentTheme() {
30
+ const themes = getAvailableThemes();
31
+ const storedName = localStorage.getItem(STORAGE_KEY_NAME);
32
+ const storedMode = localStorage.getItem(STORAGE_KEY_MODE);
33
+
34
+ // If we have stored theme, validate it exists in available themes
35
+ if (storedName && storedMode) {
36
+ const found = themes.find(t => t.name === storedName && t.mode === storedMode);
37
+ if (found) {
38
+ return found;
39
+ }
40
+ }
41
+
42
+ // Return first available theme as default
43
+ return themes[0];
44
+ }
45
+
46
+ // Apply theme to HTML element
47
+ function applyTheme(theme) {
48
+ const html = document.documentElement;
49
+ html.setAttribute('data-theme-name', theme.name);
50
+ html.setAttribute('data-theme-mode', theme.mode);
51
+
52
+ // Store in localStorage
53
+ localStorage.setItem(STORAGE_KEY_NAME, theme.name);
54
+ localStorage.setItem(STORAGE_KEY_MODE, theme.mode);
55
+
56
+ // Set cookie for server-side access
57
+ document.cookie = `theme_name=${theme.name}; path=/; max-age=31536000`; // 1 year
58
+ document.cookie = `theme_mode=${theme.mode}; path=/; max-age=31536000`;
59
+
60
+ // Optional: Send to backend if API endpoint is configured
61
+ const apiEndpoint = document.querySelector('[data-theme-api-endpoint]');
62
+ if (apiEndpoint && apiEndpoint.dataset.themeApiEndpoint) {
63
+ sendThemeToBackend(theme, apiEndpoint.dataset.themeApiEndpoint);
64
+ }
65
+
66
+ // Update UI elements
67
+ updateThemeUI(theme);
68
+ }
69
+
70
+ // Send theme to backend via AJAX
71
+ function sendThemeToBackend(theme, endpoint) {
72
+ fetch(endpoint, {
73
+ method: 'PATCH',
74
+ headers: {
75
+ 'Content-Type': 'application/json',
76
+ 'X-CSRF-Token': document.querySelector('[name="csrf-token"]')?.content || ''
77
+ },
78
+ body: JSON.stringify({
79
+ theme_name: theme.name,
80
+ theme_mode: theme.mode
81
+ })
82
+ }).catch(function(error) {
83
+ console.error('Failed to save theme to backend:', error);
84
+ });
85
+ }
86
+
87
+ // Update theme switcher UI (icons, labels, checkmarks)
88
+ function updateThemeUI(currentTheme) {
89
+ const themes = getAvailableThemes();
90
+
91
+ // Update toggle button icon (for 2-theme mode)
92
+ const themeIcon = document.getElementById('theme-icon');
93
+ if (themeIcon) {
94
+ // Show moon for light mode (switch to dark), sun for dark mode (switch to light)
95
+ themeIcon.innerHTML = currentTheme.mode === 'light'
96
+ ? '<%= render "shared/icons/moon", css_class: "w-4 h-4" %>' // Switch to dark
97
+ : '<%= render "shared/icons/sun", css_class: "w-4 h-4" %>'; // Switch to light
98
+ }
99
+
100
+ // Update checkmarks in dropdown (for 3+ theme mode)
101
+ const themeItems = document.querySelectorAll('[data-theme-item]');
102
+ themeItems.forEach(function(item) {
103
+ const themeName = item.dataset.themeName;
104
+ const themeMode = item.dataset.themeMode;
105
+ const isActive = themeName === currentTheme.name && themeMode === currentTheme.mode;
106
+
107
+ const checkmark = item.querySelector('[data-theme-checkmark]');
108
+ if (checkmark) {
109
+ checkmark.style.display = isActive ? 'inline' : 'none';
110
+ }
111
+
112
+ if (isActive) {
113
+ item.setAttribute('aria-current', 'true');
114
+ } else {
115
+ item.removeAttribute('aria-current');
116
+ }
117
+ });
118
+ }
119
+
120
+ // Handle theme switching
121
+ function switchTheme(theme) {
122
+ applyTheme(theme);
123
+ }
124
+
125
+ // Handle toggle button click (2-theme mode)
126
+ function handleToggleClick() {
127
+ const themes = getAvailableThemes();
128
+ if (themes.length !== 2) return;
129
+
130
+ const current = getCurrentTheme();
131
+ // Switch to the other theme
132
+ const nextTheme = themes.find(t => t.name !== current.name || t.mode !== current.mode) || themes[0];
133
+ switchTheme(nextTheme);
134
+ }
135
+
136
+ // Initialize theme switcher
137
+ function initializeThemeSwitcher() {
138
+ const currentTheme = getCurrentTheme();
139
+
140
+ // Apply theme on page load
141
+ applyTheme(currentTheme);
142
+
143
+ // Set up toggle button (2-theme mode)
144
+ const toggleButton = document.getElementById('theme-toggle');
145
+ if (toggleButton) {
146
+ toggleButton.addEventListener('click', handleToggleClick);
147
+ }
148
+
149
+ // Set up dropdown items (3+ theme mode)
150
+ const themeItems = document.querySelectorAll('[data-theme-item]');
151
+ themeItems.forEach(function(item) {
152
+ item.addEventListener('click', function(event) {
153
+ event.preventDefault();
154
+ const themeName = item.dataset.themeName;
155
+ const themeMode = item.dataset.themeMode;
156
+ const theme = getAvailableThemes().find(t => t.name === themeName && t.mode === themeMode);
157
+ if (theme) {
158
+ switchTheme(theme);
159
+ }
160
+ });
161
+ });
162
+ }
163
+
164
+ // Initialize on DOMContentLoaded
165
+ if (document.readyState === 'loading') {
166
+ document.addEventListener('DOMContentLoaded', initializeThemeSwitcher);
167
+ } else {
168
+ initializeThemeSwitcher();
169
+ }
170
+
171
+ // Re-initialize on Turbo page loads
172
+ if (typeof Turbo !== 'undefined') {
173
+ document.addEventListener('turbo:load', initializeThemeSwitcher);
174
+ }
175
+
176
+ // Re-initialize on Turbolinks page loads (legacy)
177
+ document.addEventListener('turbolinks:load', initializeThemeSwitcher);
178
+
179
+ // Export functions for external use
180
+ window.NeonSakura = window.NeonSakura || {};
181
+ window.NeonSakura.Theme = {
182
+ initialize: initializeThemeSwitcher,
183
+ getCurrentTheme: getCurrentTheme,
184
+ switchTheme: switchTheme,
185
+ applyTheme: applyTheme
186
+ };
187
+ })();
@@ -0,0 +1,105 @@
1
+ <div class="min-h-screen bg-gray-900 text-white flex items-center justify-center px-4">
2
+ <div class="max-w-2xl w-full">
3
+ <!-- Error Content -->
4
+ <div class="text-center">
5
+ <!-- Error Icon -->
6
+ <div class="mb-8 flex justify-center">
7
+ <% case @error_code %>
8
+ <% when 400, 422 %>
9
+ <%= render "shared/icons/alert_triangle", css_class: "w-24 h-24 text-yellow-400" %>
10
+ <% when 401, 403 %>
11
+ <%= render "shared/icons/shield_check", css_class: "w-24 h-24 text-red-400" %>
12
+ <% when 404 %>
13
+ <%= render "shared/icons/file_question", css_class: "w-24 h-24 text-cyan-400" %>
14
+ <% when 500, 503 %>
15
+ <%= render "shared/icons/server_error", css_class: "w-24 h-24 text-red-400" %>
16
+ <% else %>
17
+ <%= render "shared/icons/alert_circle", css_class: "w-24 h-24 text-gray-400" %>
18
+ <% end %>
19
+ </div>
20
+
21
+ <!-- Error Code -->
22
+ <div class="mb-4">
23
+ <h1 class="text-6xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
24
+ <%= @error_code %>
25
+ </h1>
26
+ </div>
27
+
28
+ <!-- Error Title -->
29
+ <h2 class="text-3xl font-bold text-white mb-4">
30
+ <%= @error_title %>
31
+ </h2>
32
+
33
+ <!-- Error Message -->
34
+ <p class="text-lg text-gray-300 mb-8 max-w-lg mx-auto">
35
+ <%= @error_message %>
36
+ </p>
37
+
38
+ <!-- Error Details (if provided) -->
39
+ <% if @error_details.present? %>
40
+ <div class="bg-gray-800 rounded-lg p-6 border border-gray-700 mb-8 text-left max-w-xl mx-auto">
41
+ <h3 class="text-lg font-semibold text-white mb-3 flex items-center">
42
+ <%= render "shared/icons/alert_circle", css_class: "w-5 h-5 text-yellow-400 mr-2" %>
43
+ Additional Details
44
+ </h3>
45
+ <div class="text-sm text-gray-400">
46
+ <% if @error_details.is_a?(Array) %>
47
+ <ul class="list-disc list-inside space-y-1">
48
+ <% @error_details.each do |detail| %>
49
+ <li><%= detail %></li>
50
+ <% end %>
51
+ </ul>
52
+ <% elsif @error_details.is_a?(Hash) %>
53
+ <dl class="space-y-2">
54
+ <% @error_details.each do |key, value| %>
55
+ <div>
56
+ <dt class="font-semibold text-gray-300"><%= key.to_s.titleize %>:</dt>
57
+ <dd class="ml-4">
58
+ <% if value.is_a?(Array) %>
59
+ <ul class="list-disc list-inside">
60
+ <% value.each do |v| %>
61
+ <li><%= v %></li>
62
+ <% end %>
63
+ </ul>
64
+ <% else %>
65
+ <%= value %>
66
+ <% end %>
67
+ </dd>
68
+ </div>
69
+ <% end %>
70
+ </dl>
71
+ <% else %>
72
+ <p><%= @error_details %></p>
73
+ <% end %>
74
+ </div>
75
+ </div>
76
+ <% end %>
77
+
78
+ <!-- Action Buttons -->
79
+ <div class="flex flex-col sm:flex-row gap-4 justify-center items-center">
80
+ <%= link_to root_path,
81
+ class: "px-6 py-3 bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white rounded-lg font-medium transition-all inline-flex items-center",
82
+ "aria-label": "Go to homepage" do %>
83
+ <%= render "shared/icons/arrow_up", css_class: "w-5 h-5 mr-2 transform rotate-180" %>
84
+ Go Home
85
+ <% end %>
86
+
87
+ <button
88
+ onclick="window.history.back()"
89
+ class="px-6 py-3 bg-gray-700 hover:bg-gray-600 text-white rounded-lg font-medium transition-colors inline-flex items-center"
90
+ aria-label="Go back to previous page"
91
+ >
92
+ <%= render "shared/icons/arrow_up", css_class: "w-5 h-5 mr-2 transform rotate-180" %>
93
+ Go Back
94
+ </button>
95
+ </div>
96
+
97
+ <!-- Help Text -->
98
+ <div class="mt-8 text-sm text-gray-500">
99
+ <p>
100
+ If this problem persists, please contact support.
101
+ </p>
102
+ </div>
103
+ </div>
104
+ </div>
105
+ </div>
@@ -0,0 +1,19 @@
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
+ <title><%= @error_code %> - <%= @error_title %> | Artemis</title>
7
+
8
+ <link rel="icon" href="/icon.png" type="image/png">
9
+ <link rel="icon" href="/icon.svg" type="image/svg+xml">
10
+
11
+ <%= stylesheet_link_tag "base", "data-turbo-track": "reload" %>
12
+ <%= stylesheet_link_tag "components", "data-turbo-track": "reload" %>
13
+ <%= stylesheet_link_tag "forms", "data-turbo-track": "reload" %>
14
+ </head>
15
+
16
+ <body>
17
+ <%= yield %>
18
+ </body>
19
+ </html>
@@ -0,0 +1,14 @@
1
+ <% if NeonSakura.config.nav_links.blank? %>
2
+ <%# Only show default Mission Control application selection when custom nav is NOT configured %>
3
+ <% if defined?(MissionControl::Jobs) && MissionControl::Jobs.applications.size > 1 %>
4
+ <div class="tabs">
5
+ <ul>
6
+ <% MissionControl::Jobs.applications.each do |application| %>
7
+ <li class="<%= "is-active" if application == Current.application %>">
8
+ <%= link_to application.name, mission_control_jobs.application_jobs_path(application) %>
9
+ </li>
10
+ <% end %>
11
+ </ul>
12
+ </div>
13
+ <% end %>
14
+ <% end %>
@@ -0,0 +1,21 @@
1
+ <% if NeonSakura.config.nav_links.present? %>
2
+ <!-- Custom back link and auto-refresh inline section -->
3
+ <div class="mc-back-section">
4
+ <div class="container">
5
+ <a href="/">
6
+ <%= render "shared/icons/arrow_left" %>
7
+ Back to <%= NeonSakura.config.app_name %>
8
+ </a>
9
+ <button id="auto-refresh-btn">
10
+ <%= render "shared/icons/refresh" %>
11
+ Start Auto-refresh
12
+ </button>
13
+ </div>
14
+ </div>
15
+ <% else %>
16
+ <!-- Default Mission Control back link -->
17
+ <a href="/">
18
+ <%= render "shared/icons/arrow_left" %>
19
+ Back to <%= NeonSakura.config.app_name %>
20
+ </a>
21
+ <% end %>