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,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NeonSakura
4
+ class Configuration
5
+ attr_accessor :app_name, :show_version, :show_header, :nav_links, :enable_error_pages,
6
+ :nav_position, :app_icon, :show_search, :default_theme, :available_themes,
7
+ :enable_theme_persistence, :theme_api_endpoint, :enable_style_guide,
8
+ :style_guide_navbar_position, :style_guide_pagy_examples, :sticky_navbar,
9
+ :version_module
10
+
11
+ def initialize
12
+ @app_name = "My Application"
13
+ @show_version = true
14
+ @show_header = true
15
+ @enable_error_pages = true
16
+ @nav_position = :under_heading
17
+ @app_icon = nil
18
+ @show_search = false
19
+ @version_module = nil # Optional module that provides .commit_hash (e.g., YourApp::Version)
20
+ @nav_links = [
21
+ { name: "Home", path: "/" },
22
+ { name: "About", path: "/about" }
23
+ ]
24
+
25
+ # Theme configuration (v0.1.1+)
26
+ # By default, all 4 themes are available
27
+ @default_theme = { name: "purple", mode: "dark" }
28
+ @available_themes = [
29
+ { name: "green", mode: "light", label: "Green Light" },
30
+ { name: "green", mode: "dark", label: "Green Dark" },
31
+ { name: "purple", mode: "light", label: "Purple Light" },
32
+ { name: "purple", mode: "dark", label: "Purple Dark" }
33
+ ]
34
+ @enable_theme_persistence = true
35
+ @theme_api_endpoint = nil
36
+
37
+ # Style guide configuration (v0.1.3+)
38
+ @enable_style_guide = true
39
+ @style_guide_navbar_position = :end
40
+ @style_guide_pagy_examples = true # Auto-detect: show if Pagy available, skip if not
41
+
42
+ # Navbar configuration
43
+ @sticky_navbar = false # Off by default for backward compatibility
44
+ end
45
+
46
+ # Auto-inject style guide link into nav_links
47
+ # Called by engine initializer in development/test environments
48
+ def inject_style_guide_link!
49
+ return unless enable_style_guide
50
+ return unless Rails.env.development? || Rails.env.test?
51
+
52
+ # Don't inject twice
53
+ return if nav_links.any? { |link| link[:path] == "/style-guide" }
54
+
55
+ style_guide_link = {
56
+ type: "link",
57
+ name: "Style Guide",
58
+ path: "/style-guide",
59
+ icon: "palette",
60
+ condition: "Rails.env.development? || Rails.env.test?",
61
+ aria_label: "View style guide"
62
+ }
63
+
64
+ case style_guide_navbar_position
65
+ when :start
66
+ nav_links.unshift(style_guide_link)
67
+ when :before_logout
68
+ logout_index = nav_links.find_index { |link| link[:type] == "button" }
69
+ if logout_index
70
+ nav_links.insert(logout_index, style_guide_link)
71
+ else
72
+ nav_links << style_guide_link
73
+ end
74
+ when Integer
75
+ nav_links.insert(style_guide_navbar_position, style_guide_link)
76
+ else # :end or default
77
+ nav_links << style_guide_link
78
+ end
79
+ end
80
+ end
81
+
82
+ class << self
83
+ attr_accessor :configuration
84
+ end
85
+
86
+ def self.configure
87
+ self.configuration ||= Configuration.new
88
+ yield(configuration) if block_given?
89
+ end
90
+
91
+ def self.config
92
+ configuration || Configuration.new
93
+ end
94
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support"
4
+ require "action_dispatch"
5
+ require "rails/engine"
6
+ require "propshaft"
7
+
8
+ module NeonSakura
9
+ class Engine < ::Rails::Engine
10
+ # Initialize the engine
11
+ config.assets.enabled = true
12
+
13
+ # Ensure stylesheets are added to asset paths
14
+ initializer "neon_sakura.assets" do |app|
15
+ if app.config.respond_to?(:assets)
16
+ app.config.assets.paths << root.join("app", "assets", "stylesheets")
17
+ end
18
+ end
19
+
20
+ # Add engine views to application view paths
21
+ initializer "neon_sakura.view_paths" do |app|
22
+ ActiveSupport.on_load(:action_controller) do
23
+ append_view_path(NeonSakura::Engine.root.join("app", "views"))
24
+ end
25
+
26
+ ActiveSupport.on_load(:action_mailer) do
27
+ append_view_path(NeonSakura::Engine.root.join("app", "views"))
28
+ end
29
+ end
30
+
31
+ # Include helpers in ActionView
32
+ initializer "neon_sakura.helpers" do
33
+ ActiveSupport.on_load(:action_view) do
34
+ include NeonSakura::IconHelper
35
+ include NeonSakura::ThemeHelper
36
+ include NeonSakura::StylesheetHelper
37
+ include NeonSakura::ProfileHelper
38
+ end
39
+ end
40
+
41
+ # Auto-inject style guide link into navbar (development/test only)
42
+ initializer "neon_sakura.style_guide_navbar", after: :load_config_initializers do
43
+ if NeonSakura.configuration.enable_style_guide && (Rails.env.development? || Rails.env.test?)
44
+ NeonSakura.configuration.inject_style_guide_link!
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NeonSakura
4
+ module IconHelper
5
+ # Renders an icon partial with the given options
6
+ # @param name [String] The icon name (without underscore prefix)
7
+ # @param options [Hash] Options for the icon
8
+ # @option options [String] :css_class CSS class for the icon
9
+ # @option options [Boolean] :aria_hidden Whether the icon is aria-hidden
10
+ # @option options [String] :aria_label Label for accessibility
11
+ # All other options are passed through to the icon partial as local_assigns
12
+ # @return [String] Rendered icon partial
13
+ def render_icon(name, options = {})
14
+ # Set default CSS class if not provided
15
+ options[:css_class] ||= "w-4 h-4"
16
+
17
+ # Pass all options to the partial as local_assigns
18
+ render "shared/icons/#{name}", options
19
+ end
20
+
21
+ # Get a list of all available icons
22
+ # @return [Array<String>] Array of icon names
23
+ def available_icons
24
+ # Use gem's icon directory when running within the gem, otherwise use app's directory
25
+ root_path = defined?(NeonSakura::Engine) ? NeonSakura::Engine.root : Rails.root
26
+ icon_files = Dir.glob(File.join(root_path, "app", "views", "shared", "icons", "_*.html.erb"))
27
+ icon_files.map do |file|
28
+ File.basename(file, ".html.erb").gsub(/^_/, "")
29
+ end.sort
30
+ end
31
+
32
+ # Check if an icon exists
33
+ # @param name [String] The icon name to check
34
+ # @return [Boolean] Whether the icon exists
35
+ def icon_exists?(name)
36
+ # Use gem's icon directory when running within the gem, otherwise use app's directory
37
+ root_path = defined?(NeonSakura::Engine) ? NeonSakura::Engine.root : Rails.root
38
+ File.exist?(File.join(root_path, "app", "views", "shared", "icons", "_#{name}.html.erb"))
39
+ end
40
+
41
+ # Renders a list of icons with consistent styling
42
+ # @param icon_names [Array<String>] Array of icon names
43
+ # @param options [Hash] Options for rendering
44
+ # @return [String] HTML string with rendered icons
45
+ def render_icon_list(icon_names, options = {})
46
+ css_class = options[:css_class] || "flex items-center gap-2"
47
+ icons_html = icon_names.map do |icon_name|
48
+ render_icon(icon_name, options.merge(css_class: options[:icon_css_class]))
49
+ end.join("\n")
50
+
51
+ "<div class=\"#{css_class}\">#{icons_html}</div>".html_safe
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NeonSakura
4
+ module ProfileHelper
5
+ # Generate Gravatar URL for email address
6
+ #
7
+ # @param email [String] Email address to generate Gravatar for
8
+ # @param size [Integer] Image size in pixels (default: 80)
9
+ # @return [String] Gravatar URL
10
+ #
11
+ # @example
12
+ # gravatar_url("user@example.com") # => "https://www.gravatar.com/avatar/..."
13
+ # gravatar_url("user@example.com", size: 120) # => "https://www.gravatar.com/avatar/...?s=120"
14
+ #
15
+ # Note: MD5 is used for Gravatar GUID generation (public identifier, not sensitive data)
16
+ def gravatar_url(email, size: 80)
17
+ return "" if email.blank?
18
+
19
+ require "digest/md5"
20
+ hash = Digest::MD5.hexdigest(email.downcase.strip) # nosemgrep: weak-hashes-md5
21
+ "https://www.gravatar.com/avatar/#{hash}?s=#{size}&d=identicon"
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NeonSakura
4
+ module StylesheetHelper
5
+ # Load all neon_sakura stylesheets in the correct cascade order
6
+ # This helper is needed because Propshaft doesn't process @import statements
7
+ # like Sprockets did. Each CSS file must be loaded individually.
8
+ #
9
+ # Usage in your layout:
10
+ # <%= neon_sakura_stylesheets %>
11
+ def neon_sakura_stylesheets
12
+ files = [
13
+ # Themes first - defines CSS variables
14
+ "theme-default",
15
+ "theme-purple",
16
+ "theme-green",
17
+ "theme-red",
18
+ # Utilities - uses CSS variables
19
+ "utility-reset",
20
+ "utility-layout",
21
+ "utility-sizing",
22
+ "utility-spacing",
23
+ "utility-colors",
24
+ "utility-borders",
25
+ "utility-typography",
26
+ "utility-gradients",
27
+ "utility-effects",
28
+ "utility-responsive",
29
+ # Loading indicators
30
+ "loading",
31
+ # Components and forms - uses both
32
+ "components",
33
+ "forms",
34
+ "pagy-tailwind"
35
+ ]
36
+
37
+ safe_join(files.map { |file| stylesheet_link_tag(file, "data-turbo-track": "reload") })
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NeonSakura
4
+ module ThemeHelper
5
+ # Returns the current theme as a hash with :name and :mode keys
6
+ # Priority: session/cookies (from JS localStorage) -> default_theme from config
7
+ def current_theme
8
+ # Try to get theme from session (set by JavaScript via cookie)
9
+ theme_name = cookies[:theme_name] || session[:theme_name]
10
+ theme_mode = cookies[:theme_mode] || session[:theme_mode]
11
+
12
+ if theme_name && theme_mode
13
+ { name: theme_name.to_s, mode: theme_mode.to_s }
14
+ else
15
+ # Fall back to default theme from configuration
16
+ NeonSakura.config.default_theme
17
+ end
18
+ end
19
+
20
+ # Returns HTML data attributes for the current theme
21
+ # Usage: <html <%= theme_data_attributes %>>
22
+ def theme_data_attributes
23
+ theme = current_theme
24
+ "data-theme-name='#{theme[:name]}' data-theme-mode='#{theme[:mode]}'".html_safe
25
+ end
26
+
27
+ # Returns available themes as JSON for JavaScript
28
+ def available_themes_json
29
+ NeonSakura.config.available_themes.to_json.html_safe
30
+ end
31
+
32
+ # Returns the number of available themes
33
+ def theme_count
34
+ NeonSakura.config.available_themes.length
35
+ end
36
+
37
+ # Helper to determine theme switcher mode
38
+ # Returns: :disabled (1 theme), :toggle (2 themes), :dropdown (3+ themes)
39
+ def theme_switcher_mode
40
+ case theme_count
41
+ when 0, 1
42
+ :disabled
43
+ when 2
44
+ :toggle
45
+ else
46
+ :dropdown
47
+ end
48
+ end
49
+
50
+ # Renders an icon partial for the current theme
51
+ # @param name [String] The icon name (without underscore prefix)
52
+ # @param options [Hash] Options for the icon
53
+ # All options are passed through to the icon partial as local_assigns
54
+ # @return [String] Rendered icon partial
55
+ def render_theme_icon(name, options = {})
56
+ # Set default CSS class if not provided
57
+ options[:css_class] ||= "w-4 h-4"
58
+
59
+ # Pass all options to the partial as local_assigns
60
+ render "shared/icons/#{name}", options
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NeonSakura
4
+ # ThemeImporter provides utilities to import themes from other CSS frameworks
5
+ # like Bootstrap, Material UI, etc. and convert them to neon_sakura's theme structure
6
+ class ThemeImporter
7
+ # Import a Bootstrap theme
8
+ # @param bootstrap_vars [Hash] Bootstrap color variables
9
+ # @return [Hash] neon_sakura theme structure
10
+ def self.from_bootstrap(bootstrap_vars)
11
+ {
12
+ name: bootstrap_vars[:name] || "bootstrap",
13
+ mode: bootstrap_vars[:mode] || detect_mode(bootstrap_vars),
14
+ label: bootstrap_vars[:label] || "Bootstrap Theme",
15
+ colors: {
16
+ accent: bootstrap_vars[:primary] || "#0d6efd",
17
+ background: bootstrap_vars[:"body-bg"] || "#ffffff",
18
+ surface: bootstrap_vars[:light] || "#f8f9fa",
19
+ text_primary: bootstrap_vars[:"body-color"] || "#212529",
20
+ text_secondary: bootstrap_vars[:secondary] || "#6c757d",
21
+ notification: bootstrap_vars[:success] || "#198754",
22
+ alert: bootstrap_vars[:danger] || "#dc3545",
23
+ border: bootstrap_vars[:border] || "#dee2e6"
24
+ }
25
+ }
26
+ end
27
+
28
+ # Import a Material UI theme
29
+ # @param material_vars [Hash] Material UI palette variables
30
+ # @return [Hash] neon_sakura theme structure
31
+ def self.from_material_ui(material_vars)
32
+ palette = material_vars[:palette] || {}
33
+
34
+ {
35
+ name: material_vars[:name] || "material",
36
+ mode: material_vars[:mode] || (palette[:mode] == "dark" ? "dark" : "light"),
37
+ label: material_vars[:label] || "Material Theme",
38
+ colors: {
39
+ accent: palette.dig(:primary, :main) || "#1976d2",
40
+ background: palette.dig(:background, :default) || "#ffffff",
41
+ surface: palette.dig(:background, :paper) || "#ffffff",
42
+ text_primary: palette.dig(:text, :primary) || "rgba(0, 0, 0, 0.87)",
43
+ text_secondary: palette.dig(:text, :secondary) || "rgba(0, 0, 0, 0.6)",
44
+ notification: palette.dig(:success, :main) || "#2e7d32",
45
+ alert: palette.dig(:error, :main) || "#d32f2f",
46
+ border: palette.dig(:divider) || "rgba(0, 0, 0, 0.12)"
47
+ }
48
+ }
49
+ end
50
+
51
+ # Import from a generic hash of color variables
52
+ # @param theme_hash [Hash] Arbitrary theme hash
53
+ # @return [Hash] neon_sakura theme structure
54
+ def self.from_hash(theme_hash)
55
+ {
56
+ name: theme_hash[:name] || "custom",
57
+ mode: theme_hash[:mode] || detect_mode(theme_hash),
58
+ label: theme_hash[:label] || "Custom Theme",
59
+ colors: {
60
+ accent: theme_hash[:accent] || theme_hash[:primary] || "#3b82f6",
61
+ background: theme_hash[:background] || theme_hash[:bg] || "#ffffff",
62
+ surface: theme_hash[:surface] || theme_hash[:card] || "#f9fafb",
63
+ text_primary: theme_hash[:text_primary] || theme_hash[:text] || "#111827",
64
+ text_secondary: theme_hash[:text_secondary] || "#6b7280",
65
+ notification: theme_hash[:notification] || theme_hash[:success] || "#10b981",
66
+ alert: theme_hash[:alert] || theme_hash[:error] || theme_hash[:danger] || "#ef4444",
67
+ border: theme_hash[:border] || "#e5e7eb"
68
+ }
69
+ }
70
+ end
71
+
72
+ # Generate CSS custom properties for a theme
73
+ # @param theme [Hash] Theme structure from import methods
74
+ # @return [String] CSS custom properties
75
+ def self.to_css_variables(theme)
76
+ colors = theme[:colors]
77
+ css = ":root[data-theme-name=\"#{theme[:name]}\"][data-theme-mode=\"#{theme[:mode]}\"] {\n"
78
+
79
+ colors.each do |key, value|
80
+ css_var_name = key.to_s.gsub("_", "-")
81
+ css += " --color-#{css_var_name}: #{value};\n"
82
+ end
83
+
84
+ css += "}\n"
85
+ css
86
+ end
87
+
88
+ private
89
+
90
+ # Detect light/dark mode based on background color brightness
91
+ # @param theme_vars [Hash] Theme color variables
92
+ # @return [String] "light" or "dark"
93
+ def self.detect_mode(theme_vars)
94
+ bg = theme_vars[:background] || theme_vars[:"body-bg"] || "#ffffff"
95
+
96
+ # Convert hex to RGB and calculate brightness
97
+ if bg.start_with?("#")
98
+ hex = bg.delete("#")
99
+ r = hex[0..1].to_i(16)
100
+ g = hex[2..3].to_i(16)
101
+ b = hex[4..5].to_i(16)
102
+
103
+ # Calculate perceived brightness (0-255)
104
+ brightness = (r * 0.299 + g * 0.587 + b * 0.114)
105
+
106
+ brightness > 127 ? "light" : "dark"
107
+ else
108
+ "light" # Default to light if we can't parse the color
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NeonSakura
4
+ VERSION = "0.1.4"
5
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "neon_sakura/version"
4
+ require_relative "neon_sakura/configuration"
5
+ require_relative "neon_sakura/icon_helper"
6
+ require_relative "neon_sakura/theme_helper"
7
+ require_relative "neon_sakura/stylesheet_helper"
8
+ require_relative "neon_sakura/profile_helper"
9
+ require_relative "neon_sakura/engine"
10
+
11
+ module NeonSakura
12
+ class Error < StandardError; end
13
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'neon_sakura/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'neon_sakura'
9
+ spec.version = NeonSakura::VERSION
10
+ spec.authors = [ 'trex22' ]
11
+ spec.email = [ 'contact@jasonchalom.com' ]
12
+
13
+ spec.summary = 'Shared styling and theming components for Rails applications'
14
+ spec.description = 'A gem that extracts the styling and theming components from my Rails applications for reuse across multiple projects'
15
+ spec.homepage = 'https://github.com/trex22/neon_sakura'
16
+ spec.license = 'MIT'
17
+ spec.required_ruby_version = Gem::Requirement.new('>= 3.4.7')
18
+
19
+ spec.metadata['source_code_uri'] = 'https://github.com/trex22/neon_sakura'
20
+ spec.metadata['changelog_uri'] = 'https://github.com/trex22/neon_sakura/blob/main/CHANGELOG.md'
21
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/trex22/neon_sakura/issues'
22
+
23
+ # Specify which files should be added to the gem when it is released.
24
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
+ end
27
+
28
+ spec.bindir = 'exe'
29
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
+ spec.require_paths = [ 'lib' ]
31
+
32
+ # Dependencies
33
+ spec.add_dependency 'rails', '>= 8.0.0'
34
+ spec.add_dependency 'propshaft', '>= 1.3.1'
35
+
36
+ # Development dependencies
37
+ spec.add_development_dependency 'brakeman', '~> 7.1.1'
38
+ spec.add_development_dependency 'bundler', '~> 2.7.2'
39
+ spec.add_development_dependency 'bundler-audit', '~> 0.9.3'
40
+ spec.add_development_dependency 'minitest', '~> 6.0.1'
41
+ spec.add_development_dependency 'minitest-focus', '~> 1.4.0'
42
+ spec.add_development_dependency 'minitest-reporters', '~> 1.7.1'
43
+ spec.add_development_dependency 'mocha', '~> 3.0.1'
44
+ spec.add_development_dependency 'pry', '~> 0.16.0'
45
+ spec.add_development_dependency 'rake', '~> 13.3.1'
46
+ spec.add_development_dependency 'rubocop-rails-omakase', '~> 1.1.0'
47
+ spec.add_development_dependency 'simplecov', '~> 0.22.0'
48
+ spec.add_development_dependency 'timecop', '~> 0.9.10'
49
+ spec.add_development_dependency 'webmock', '~> 3.26.1'
50
+ end
data/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "neon_sakura",
3
+ "version": "0.1.2",
4
+ "description": "A Rails engine for reusable styling and theming components",
5
+ "private": true,
6
+ "scripts": {
7
+ "lint:css": "stylelint 'app/assets/stylesheets/**/*.css'",
8
+ "lint:css:fix": "stylelint 'app/assets/stylesheets/**/*.css' --fix"
9
+ },
10
+ "devDependencies": {
11
+ "stylelint": "^16.12.0",
12
+ "stylelint-config-standard": "^36.0.1"
13
+ },
14
+ "engines": {
15
+ "node": ">=18.0.0",
16
+ "yarn": ">=1.22.0"
17
+ }
18
+ }
@@ -0,0 +1,132 @@
1
+ #!/bin/bash
2
+
3
+ # Neon Sakura post-merge hook
4
+ # Auto-runs database migrations and asset precompilation after merges
5
+
6
+ echo "🔄 Running post-merge tasks..."
7
+
8
+ # Check if we're in a Rails app
9
+ if [ ! -f "Gemfile" ]; then
10
+ echo "❌ Not a Rails application, skipping post-merge tasks"
11
+ exit 0
12
+ fi
13
+
14
+ # Colors for output
15
+ RED='\033[0;31m'
16
+ GREEN='\033[0;32m'
17
+ YELLOW='\033[1;33m'
18
+ BLUE='\033[0;34m'
19
+ NC='\033[0m' # No Color
20
+
21
+ # Track if any tasks were performed
22
+ tasks_performed=0
23
+
24
+ # 1. Check for new migrations and run them
25
+ check_and_run_migrations() {
26
+ echo -e "${BLUE}Checking for new migrations...${NC}"
27
+
28
+ # Check if there are pending migrations
29
+ pending_migrations=$(bundle exec rails db:migrate:status 2>/dev/null | grep "^\s*down" | wc -l)
30
+
31
+ if [ "$pending_migrations" -gt 0 ]; then
32
+ echo -e "${YELLOW}Found $pending_migrations pending migration(s)${NC}"
33
+ echo -e "${BLUE}Running migrations...${NC}"
34
+
35
+ if bundle exec rails db:migrate; then
36
+ echo -e "${GREEN}✅ Migrations completed successfully${NC}"
37
+ tasks_performed=$((tasks_performed + 1))
38
+ else
39
+ echo -e "${RED}❌ Migration failed${NC}"
40
+ echo -e "${RED}Please run 'bundle exec rails db:migrate' manually${NC}"
41
+ return 1
42
+ fi
43
+ else
44
+ echo -e "${GREEN}✅ No pending migrations${NC}"
45
+ fi
46
+
47
+ return 0
48
+ }
49
+
50
+ # 2. Check for Gemfile changes and run bundle install
51
+ check_and_bundle_install() {
52
+ echo -e "${BLUE}Checking for Gemfile changes...${NC}"
53
+
54
+ # Check if Gemfile or Gemfile.lock changed in the merge
55
+ if git diff HEAD@{1} HEAD --name-only | grep -E "^Gemfile|^Gemfile\.lock$" > /dev/null; then
56
+ echo -e "${YELLOW}Gemfile changes detected${NC}"
57
+ echo -e "${BLUE}Running bundle install...${NC}"
58
+
59
+ if bundle install; then
60
+ echo -e "${GREEN}✅ Bundle install completed${NC}"
61
+ tasks_performed=$((tasks_performed + 1))
62
+ else
63
+ echo -e "${RED}❌ Bundle install failed${NC}"
64
+ echo -e "${RED}Please run 'bundle install' manually${NC}"
65
+ return 1
66
+ fi
67
+ else
68
+ echo -e "${GREEN}✅ No Gemfile changes detected${NC}"
69
+ fi
70
+
71
+ return 0
72
+ }
73
+
74
+ # 3. Check for asset changes and precompile if needed
75
+ check_and_precompile_assets() {
76
+ echo -e "${BLUE}Checking for asset changes...${NC}"
77
+
78
+ # Check if asset files changed
79
+ asset_changes=$(git diff HEAD@{1} HEAD --name-only | grep -E "^app/assets/|^app/javascript/|^config/importmap\.rb|^config/routes\.rb" | wc -l)
80
+
81
+ if [ "$asset_changes" -gt 0 ] && [ -f "config/application.rb" ]; then
82
+ # Check if we're using asset pipeline
83
+ if grep -q "propshaft" Gemfile 2>/dev/null; then
84
+ echo -e "${YELLOW}Asset changes detected${NC}"
85
+ echo -e "${BLUE}Precompiling assets...${NC}"
86
+
87
+ # Only precompile in development if specifically configured
88
+ if bundle exec rails assets:precompile RAILS_ENV=development > /dev/null 2>&1; then
89
+ echo -e "${GREEN}✅ Assets precompiled successfully${NC}"
90
+ tasks_performed=$((tasks_performed + 1))
91
+ else
92
+ echo -e "${YELLOW}⚠️ Asset precompilation skipped (likely not needed in development)${NC}"
93
+ fi
94
+ else
95
+ echo -e "${GREEN}✅ No asset pipeline detected${NC}"
96
+ fi
97
+ else
98
+ echo -e "${GREEN}✅ No asset changes detected${NC}"
99
+ fi
100
+
101
+ return 0
102
+ }
103
+
104
+ # Run all checks
105
+ failed_tasks=0
106
+
107
+ if ! check_and_bundle_install; then
108
+ failed_tasks=$((failed_tasks + 1))
109
+ fi
110
+
111
+ if ! check_and_run_migrations; then
112
+ failed_tasks=$((failed_tasks + 1))
113
+ fi
114
+
115
+ if ! check_and_precompile_assets; then
116
+ failed_tasks=$((failed_tasks + 1))
117
+ fi
118
+
119
+ # Summary
120
+ echo ""
121
+ if [ $failed_tasks -gt 0 ]; then
122
+ echo -e "${RED}❌ $failed_tasks post-merge task(s) failed${NC}"
123
+ echo -e "${YELLOW}Please address the issues above manually${NC}"
124
+ elif [ $tasks_performed -gt 0 ]; then
125
+ echo -e "${GREEN}🎉 $tasks_performed post-merge task(s) completed successfully!${NC}"
126
+ else
127
+ echo -e "${GREEN}✅ No post-merge tasks needed${NC}"
128
+ fi
129
+
130
+ # Note: We don't exit with error code for post-merge hooks
131
+ # as they shouldn't block the merge operation
132
+ exit 0