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.
- checksums.yaml +7 -0
- data/.ai-reviewer/README.md +182 -0
- data/.ai-reviewer/ai-reviewer.sh +56 -0
- data/.ai-reviewer/build-system-prompt.sh +136 -0
- data/.ai-reviewer/extract-claude-sections.sh +32 -0
- data/.ai-reviewer/test-ai-reviewer.sh +40 -0
- data/.ai-reviewer-config.yml +190 -0
- data/.github/dependabot.yml +12 -0
- data/.github/settings.yml +70 -0
- data/.github/workflows/ai-pr-review-on-comment.yml +384 -0
- data/.github/workflows/ai-pr-review.yml +328 -0
- data/.github/workflows/license-check.yml +78 -0
- data/.github/workflows/lint.yml +79 -0
- data/.github/workflows/security.yml +131 -0
- data/.github/workflows/semgrep.yml +26 -0
- data/.github/workflows/test.yml +44 -0
- data/.gitignore +75 -0
- data/.rubocop.yml +33 -0
- data/.ruby-version +1 -0
- data/.simplecov +14 -0
- data/.stylelintignore +10 -0
- data/.stylelintrc.json +37 -0
- data/AGENTS.md +51 -0
- data/CHANGELOG.md +568 -0
- data/CLAUDE.md +632 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +327 -0
- data/LICENSE +21 -0
- data/README.md +1209 -0
- data/Rakefile +25 -0
- data/app/assets/images/cherry_blossom.svg +1525 -0
- data/app/assets/images/cherry_blossom_tree.png +0 -0
- data/app/assets/images/prysm-icon.png +0 -0
- data/app/assets/stylesheets/base.css +29 -0
- data/app/assets/stylesheets/components.css +1652 -0
- data/app/assets/stylesheets/forms.css +152 -0
- data/app/assets/stylesheets/loading.css +145 -0
- data/app/assets/stylesheets/neon_sakura.css +40 -0
- data/app/assets/stylesheets/pagy-tailwind.css +120 -0
- data/app/assets/stylesheets/theme-default.css +40 -0
- data/app/assets/stylesheets/theme-green.css +84 -0
- data/app/assets/stylesheets/theme-purple.css +94 -0
- data/app/assets/stylesheets/theme-red.css +84 -0
- data/app/assets/stylesheets/utility-borders.css +29 -0
- data/app/assets/stylesheets/utility-colors.css +185 -0
- data/app/assets/stylesheets/utility-effects.css +123 -0
- data/app/assets/stylesheets/utility-gradients.css +158 -0
- data/app/assets/stylesheets/utility-layout.css +132 -0
- data/app/assets/stylesheets/utility-reset.css +13 -0
- data/app/assets/stylesheets/utility-responsive.css +145 -0
- data/app/assets/stylesheets/utility-sizing.css +99 -0
- data/app/assets/stylesheets/utility-spacing.css +174 -0
- data/app/assets/stylesheets/utility-typography.css +97 -0
- data/app/controllers/errors_controller.rb +120 -0
- data/app/controllers/style_guide_controller.rb +117 -0
- data/app/helpers/errors_helper.rb +12 -0
- data/app/helpers/neon_sakura/navbar_helper.rb +43 -0
- data/app/helpers/style_guide_helper.rb +36 -0
- data/app/javascript/neon_sakura/dropdown.js +22 -0
- data/app/javascript/neon_sakura/navbar.js +71 -0
- data/app/javascript/neon_sakura/theme_switcher.js +187 -0
- data/app/views/errors/show.html.erb +105 -0
- data/app/views/layouts/error.html.erb +19 -0
- data/app/views/layouts/mission_control/jobs/_application_selection.html.erb +14 -0
- data/app/views/layouts/mission_control/jobs/_navigation.html.erb +21 -0
- data/app/views/layouts/mission_control/jobs/application.html.erb +453 -0
- data/app/views/layouts/style_guide.html.erb +416 -0
- data/app/views/shared/_file_upload.html.erb +184 -0
- data/app/views/shared/_footer.html.erb +23 -0
- data/app/views/shared/_header.html.erb +42 -0
- data/app/views/shared/_navbar.html.erb +306 -0
- data/app/views/shared/_profile_image_selector.html.erb +165 -0
- data/app/views/shared/_theme_switcher.html.erb +64 -0
- data/app/views/shared/icons/_adjustments.html.erb +10 -0
- data/app/views/shared/icons/_alert_circle.html.erb +3 -0
- data/app/views/shared/icons/_alert_triangle.html.erb +3 -0
- data/app/views/shared/icons/_archive.html.erb +3 -0
- data/app/views/shared/icons/_arrow_down.html.erb +3 -0
- data/app/views/shared/icons/_arrow_left.html.erb +3 -0
- data/app/views/shared/icons/_arrow_up.html.erb +3 -0
- data/app/views/shared/icons/_arrows_pointing_in.html.erb +10 -0
- data/app/views/shared/icons/_arrows_pointing_out.html.erb +10 -0
- data/app/views/shared/icons/_artemis_logo.html.erb +26 -0
- data/app/views/shared/icons/_auth_banner.html.erb +1 -0
- data/app/views/shared/icons/_bars.html.erb +10 -0
- data/app/views/shared/icons/_bell.html.erb +3 -0
- data/app/views/shared/icons/_book.html.erb +3 -0
- data/app/views/shared/icons/_bookmark.html.erb +3 -0
- data/app/views/shared/icons/_box.html.erb +3 -0
- data/app/views/shared/icons/_brain.html.erb +3 -0
- data/app/views/shared/icons/_briefcase.html.erb +3 -0
- data/app/views/shared/icons/_calendar.html.erb +3 -0
- data/app/views/shared/icons/_camera.html.erb +4 -0
- data/app/views/shared/icons/_chart_bar.html.erb +3 -0
- data/app/views/shared/icons/_chart_line.html.erb +10 -0
- data/app/views/shared/icons/_chart_pie.html.erb +11 -0
- data/app/views/shared/icons/_chat.html.erb +3 -0
- data/app/views/shared/icons/_check.html.erb +3 -0
- data/app/views/shared/icons/_check_circle.html.erb +3 -0
- data/app/views/shared/icons/_cherry_blossom.html.erb +1516 -0
- data/app/views/shared/icons/_cherry_blossom_silhouette.html.erb +1016 -0
- data/app/views/shared/icons/_cherry_blossom_single_flower.html.erb +1125 -0
- data/app/views/shared/icons/_cherry_blossom_tree.html.erb +159 -0
- data/app/views/shared/icons/_chevron_down.html.erb +3 -0
- data/app/views/shared/icons/_chevron_right.html.erb +9 -0
- data/app/views/shared/icons/_clipboard.html.erb +3 -0
- data/app/views/shared/icons/_clock.html.erb +3 -0
- data/app/views/shared/icons/_close.html.erb +3 -0
- data/app/views/shared/icons/_cog.html.erb +4 -0
- data/app/views/shared/icons/_crop.html.erb +10 -0
- data/app/views/shared/icons/_crown.html.erb +3 -0
- data/app/views/shared/icons/_disc.html.erb +3 -0
- data/app/views/shared/icons/_download.html.erb +3 -0
- data/app/views/shared/icons/_dragonfly.html.erb +58 -0
- data/app/views/shared/icons/_duplicate.html.erb +4 -0
- data/app/views/shared/icons/_edit.html.erb +3 -0
- data/app/views/shared/icons/_envelope.html.erb +3 -0
- data/app/views/shared/icons/_eraser.html.erb +10 -0
- data/app/views/shared/icons/_external_link.html.erb +3 -0
- data/app/views/shared/icons/_eye.html.erb +4 -0
- data/app/views/shared/icons/_file_csv.html.erb +10 -0
- data/app/views/shared/icons/_file_export.html.erb +10 -0
- data/app/views/shared/icons/_file_image.html.erb +10 -0
- data/app/views/shared/icons/_file_import.html.erb +10 -0
- data/app/views/shared/icons/_file_question.html.erb +6 -0
- data/app/views/shared/icons/_film.html.erb +3 -0
- data/app/views/shared/icons/_filter.html.erb +3 -0
- data/app/views/shared/icons/_folder.html.erb +3 -0
- data/app/views/shared/icons/_folder_open.html.erb +3 -0
- data/app/views/shared/icons/_folder_plus.html.erb +3 -0
- data/app/views/shared/icons/_globe.html.erb +3 -0
- data/app/views/shared/icons/_google.html.erb +11 -0
- data/app/views/shared/icons/_heart.html.erb +3 -0
- data/app/views/shared/icons/_heart_broken.html.erb +11 -0
- data/app/views/shared/icons/_heart_pulse.html.erb +4 -0
- data/app/views/shared/icons/_history.html.erb +11 -0
- data/app/views/shared/icons/_home.html.erb +10 -0
- data/app/views/shared/icons/_image.html.erb +3 -0
- data/app/views/shared/icons/_inbox.html.erb +3 -0
- data/app/views/shared/icons/_info_circle.html.erb +10 -0
- data/app/views/shared/icons/_key.html.erb +3 -0
- data/app/views/shared/icons/_layers.html.erb +10 -0
- data/app/views/shared/icons/_lightbulb.html.erb +10 -0
- data/app/views/shared/icons/_lightning.html.erb +3 -0
- data/app/views/shared/icons/_list.html.erb +3 -0
- data/app/views/shared/icons/_lock.html.erb +3 -0
- data/app/views/shared/icons/_logout.html.erb +3 -0
- data/app/views/shared/icons/_magazine.html.erb +3 -0
- data/app/views/shared/icons/_magic.html.erb +3 -0
- data/app/views/shared/icons/_minus.html.erb +10 -0
- data/app/views/shared/icons/_mobile.html.erb +10 -0
- data/app/views/shared/icons/_moon.html.erb +3 -0
- data/app/views/shared/icons/_network.html.erb +10 -0
- data/app/views/shared/icons/_new_item_banner.html.erb +1 -0
- data/app/views/shared/icons/_ouroboros.html.erb +24 -0
- data/app/views/shared/icons/_package.html.erb +3 -0
- data/app/views/shared/icons/_palette.html.erb +3 -0
- data/app/views/shared/icons/_paper_plane.html.erb +10 -0
- data/app/views/shared/icons/_photo.html.erb +10 -0
- data/app/views/shared/icons/_play.html.erb +4 -0
- data/app/views/shared/icons/_plus.html.erb +3 -0
- data/app/views/shared/icons/_pocket.html.erb +11 -0
- data/app/views/shared/icons/_prysm-icon.html.erb +34 -0
- data/app/views/shared/icons/_prysm.html.erb +13 -0
- data/app/views/shared/icons/_pushbullet-1.html.erb +29 -0
- data/app/views/shared/icons/_pushbullet-2.html.erb +2 -0
- data/app/views/shared/icons/_puzzle.html.erb +10 -0
- data/app/views/shared/icons/_qrcode.html.erb +3 -0
- data/app/views/shared/icons/_question.html.erb +3 -0
- data/app/views/shared/icons/_receipt.html.erb +10 -0
- data/app/views/shared/icons/_redo.html.erb +3 -0
- data/app/views/shared/icons/_refresh.html.erb +3 -0
- data/app/views/shared/icons/_rocket.html.erb +10 -0
- data/app/views/shared/icons/_rss.html.erb +3 -0
- data/app/views/shared/icons/_save.html.erb +3 -0
- data/app/views/shared/icons/_search.html.erb +3 -0
- data/app/views/shared/icons/_search_minus.html.erb +10 -0
- data/app/views/shared/icons/_search_plus.html.erb +10 -0
- data/app/views/shared/icons/_server_error.html.erb +6 -0
- data/app/views/shared/icons/_share.html.erb +3 -0
- data/app/views/shared/icons/_shield_check.html.erb +3 -0
- data/app/views/shared/icons/_sign_in.html.erb +3 -0
- data/app/views/shared/icons/_spinner.html.erb +4 -0
- data/app/views/shared/icons/_star.html.erb +3 -0
- data/app/views/shared/icons/_store.html.erb +10 -0
- data/app/views/shared/icons/_sun.html.erb +3 -0
- data/app/views/shared/icons/_sync.html.erb +3 -0
- data/app/views/shared/icons/_table.html.erb +3 -0
- data/app/views/shared/icons/_tag.html.erb +3 -0
- data/app/views/shared/icons/_tags.html.erb +11 -0
- data/app/views/shared/icons/_tools.html.erb +4 -0
- data/app/views/shared/icons/_trash.html.erb +3 -0
- data/app/views/shared/icons/_undo.html.erb +3 -0
- data/app/views/shared/icons/_unlock.html.erb +3 -0
- data/app/views/shared/icons/_upload.html.erb +3 -0
- data/app/views/shared/icons/_user.html.erb +3 -0
- data/app/views/shared/icons/_user_circle.html.erb +10 -0
- data/app/views/shared/icons/_user_plus.html.erb +10 -0
- data/app/views/shared/icons/_video.html.erb +3 -0
- data/app/views/shared/icons/_wrench.html.erb +11 -0
- data/app/views/style_guide/index.html.erb +77 -0
- data/app/views/style_guide/sections/_alerts.html.erb +114 -0
- data/app/views/style_guide/sections/_badges.html.erb +78 -0
- data/app/views/style_guide/sections/_buttons.html.erb +130 -0
- data/app/views/style_guide/sections/_cards.html.erb +84 -0
- data/app/views/style_guide/sections/_colors.html.erb +106 -0
- data/app/views/style_guide/sections/_file_upload.html.erb +135 -0
- data/app/views/style_guide/sections/_forms.html.erb +129 -0
- data/app/views/style_guide/sections/_gradients.html.erb +253 -0
- data/app/views/style_guide/sections/_header.html.erb +12 -0
- data/app/views/style_guide/sections/_icons.html.erb +55 -0
- data/app/views/style_guide/sections/_images.html.erb +40 -0
- data/app/views/style_guide/sections/_loading.html.erb +242 -0
- data/app/views/style_guide/sections/_pagination.html.erb +212 -0
- data/app/views/style_guide/sections/_profile_components.html.erb +203 -0
- data/app/views/style_guide/sections/_theme_switcher.html.erb +72 -0
- data/app/views/style_guide/sections/_typography.html.erb +65 -0
- data/bin/ai-optimize-claude-md +540 -0
- data/bin/ai-review-local +345 -0
- data/bin/ai-security-review +585 -0
- data/bin/brakeman +9 -0
- data/bin/install-hooks +57 -0
- data/bin/rake +7 -0
- data/bin/rubocop +10 -0
- data/bin/verify_setup.rb +31 -0
- data/config/brakeman.ignore +28 -0
- data/config/initializers/neon_sakura.rb +15 -0
- data/config/license_overrides.yml +13 -0
- data/config/routes.rb +21 -0
- data/config/theme_mappings.yml +61 -0
- data/docs/PRYSM_ASSETS.md +210 -0
- data/docs/plans/extract_ai_reviewer_plan.md +151 -0
- data/docs/plans/neon_sakura_gem_plan.md +138 -0
- data/lib/neon_sakura/configuration.rb +94 -0
- data/lib/neon_sakura/engine.rb +48 -0
- data/lib/neon_sakura/icon_helper.rb +54 -0
- data/lib/neon_sakura/profile_helper.rb +24 -0
- data/lib/neon_sakura/stylesheet_helper.rb +40 -0
- data/lib/neon_sakura/theme_helper.rb +63 -0
- data/lib/neon_sakura/theme_importer.rb +112 -0
- data/lib/neon_sakura/version.rb +5 -0
- data/lib/neon_sakura.rb +13 -0
- data/neon_sakura.gemspec +50 -0
- data/package.json +18 -0
- data/scripts/git-hooks/post-merge +132 -0
- data/scripts/git-hooks/pre-commit +123 -0
- data/scripts/git-hooks/pre-push +127 -0
- data/scripts/license-check.rb +587 -0
- data/settings.local.json +12 -0
- data/yarn.lock +778 -0
- metadata +503 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
<% config = NeonSakura.config %>
|
|
2
|
+
<% return if config.nav_position == :none %>
|
|
3
|
+
|
|
4
|
+
<nav class="<%= config.sticky_navbar ? 'sticky top-0 z-50 bg-background border-b border-border' : '' %> mb-6 flex items-center justify-center flex-wrap gap-3 sm:gap-4 md:gap-6 <%= config.sticky_navbar ? 'py-4 px-4' : '' %>" role="navigation" aria-label="Main navigation">
|
|
5
|
+
<% if config.app_icon.present? %>
|
|
6
|
+
<div class="inline-flex items-center text-cyan-400">
|
|
7
|
+
<%= render_theme_icon(config.app_icon, css_class: "w-6 h-6") %>
|
|
8
|
+
</div>
|
|
9
|
+
<% end %>
|
|
10
|
+
|
|
11
|
+
<% (config.nav_links || []).each do |link| %>
|
|
12
|
+
<%
|
|
13
|
+
# Check if condition for showing this link
|
|
14
|
+
condition = link[:if] || link[:condition]
|
|
15
|
+
next if condition && !instance_eval(&condition) rescue false
|
|
16
|
+
%>
|
|
17
|
+
|
|
18
|
+
<% case link[:type] %>
|
|
19
|
+
<% when "link" %>
|
|
20
|
+
<% is_active = current_page?(link[:path]) || (link[:active_paths] && link[:active_paths].any? { |p| current_page?(p) rescue false }) rescue false %>
|
|
21
|
+
<%= link_to link[:path], class: "inline-flex items-center transition-colors text-sm whitespace-nowrap #{is_active ? 'nav-active' : 'text-navbar-link hover:text-navbar-link'}", "aria-label": link[:aria_label] || "Go to #{link[:name]}", "aria-current": (is_active ? "page" : nil), target: link[:target], rel: (link[:target] == "_blank" ? "noopener noreferrer" : nil) do %>
|
|
22
|
+
<% if link[:icon] %>
|
|
23
|
+
<%= render_theme_icon(link[:icon], css_class: "w-4 h-4 mr-2") %>
|
|
24
|
+
<% end %>
|
|
25
|
+
<%= link[:name] %>
|
|
26
|
+
<% end %>
|
|
27
|
+
|
|
28
|
+
<% when "dropdown" %>
|
|
29
|
+
<div class="nav-dropdown" data-navbar-dropdown>
|
|
30
|
+
<button class="nav-dropdown-toggle" data-action="toggle" aria-label="<%= link[:aria_label] || "#{link[:name]} menu" %>" aria-haspopup="true" aria-expanded="false">
|
|
31
|
+
<% if link[:icon] %>
|
|
32
|
+
<%= render_theme_icon(link[:icon], css_class: "w-4 h-4") %>
|
|
33
|
+
<% end %>
|
|
34
|
+
<% # Show username for user_dropdown, otherwise show link name %>
|
|
35
|
+
<% if link[:name] == "user_dropdown" && respond_to?(:current_user) && current_user %>
|
|
36
|
+
<%= current_user.email.split('@').first %>
|
|
37
|
+
<% else %>
|
|
38
|
+
<%= link[:name] %>
|
|
39
|
+
<% end %>
|
|
40
|
+
<%= render_theme_icon("chevron_down", css_class: "w-4 h-4") %>
|
|
41
|
+
</button>
|
|
42
|
+
<div class="nav-dropdown-menu" data-menu>
|
|
43
|
+
<% (link[:items] || []).each do |item| %>
|
|
44
|
+
<%
|
|
45
|
+
# Check if condition for showing this item
|
|
46
|
+
item_condition = item[:if] || item[:condition]
|
|
47
|
+
next if item_condition && !instance_eval(&item_condition) rescue false
|
|
48
|
+
%>
|
|
49
|
+
|
|
50
|
+
<% if item[:type] == "divider" %>
|
|
51
|
+
<div class="nav-dropdown-divider"></div>
|
|
52
|
+
|
|
53
|
+
<% elsif item[:type] == "theme_selector" %>
|
|
54
|
+
<%# Render theme selector as nested submenu %>
|
|
55
|
+
<div class="nav-dropdown-submenu" data-submenu-container>
|
|
56
|
+
<a href="#" class="nav-dropdown-submenu-toggle" data-submenu-toggle aria-label="Theme options">
|
|
57
|
+
<%= render_theme_icon("palette", css_class: "w-4 h-4") %>
|
|
58
|
+
Themes
|
|
59
|
+
<%= render_theme_icon("chevron_right", css_class: "w-4 h-4 ml-auto") %>
|
|
60
|
+
</a>
|
|
61
|
+
<div class="nav-dropdown-submenu-items" data-submenu>
|
|
62
|
+
<% (NeonSakura.config.available_themes || []).each do |theme| %>
|
|
63
|
+
<a href="#"
|
|
64
|
+
data-theme-item
|
|
65
|
+
data-theme-name="<%= theme[:name] %>"
|
|
66
|
+
data-theme-mode="<%= theme[:mode] %>"
|
|
67
|
+
aria-label="Switch to <%= theme[:label] %>">
|
|
68
|
+
<span data-theme-checkmark style="display: none;">
|
|
69
|
+
<%= render_theme_icon("check", css_class: "w-4 h-4") %>
|
|
70
|
+
</span>
|
|
71
|
+
<% if theme[:mode] == 'light' %>
|
|
72
|
+
<%= render_theme_icon("sun", css_class: "w-4 h-4") %>
|
|
73
|
+
<% else %>
|
|
74
|
+
<%= render_theme_icon("moon", css_class: "w-4 h-4") %>
|
|
75
|
+
<% end %>
|
|
76
|
+
<%= theme[:label] %>
|
|
77
|
+
</a>
|
|
78
|
+
<% end %>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<% else %>
|
|
83
|
+
<%# Regular dropdown item %>
|
|
84
|
+
<%= link_to item[:path], class: item[:class], "aria-label": item[:aria_label] || "Go to #{item[:name]}", target: item[:target], rel: (item[:target] == "_blank" ? "noopener noreferrer" : nil), data: { turbo_method: item[:method] } do %>
|
|
85
|
+
<% if item[:icon].present? %>
|
|
86
|
+
<%= render_theme_icon(item[:icon], css_class: "w-4 h-4") %>
|
|
87
|
+
<% end %>
|
|
88
|
+
<%= item[:name] %>
|
|
89
|
+
<% end %>
|
|
90
|
+
<% end %>
|
|
91
|
+
<% end %>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<% when "button" %>
|
|
96
|
+
<%= button_to link[:path], method: link[:method] || :post, class: "navbar-button text-sm whitespace-nowrap", "aria-label": link[:aria_label] || link[:name] do %>
|
|
97
|
+
<% if link[:icon] %>
|
|
98
|
+
<%= render_theme_icon(link[:icon], css_class: "w-4 h-4 mr-2") %>
|
|
99
|
+
<% end %>
|
|
100
|
+
<%= link[:name] %>
|
|
101
|
+
<% end %>
|
|
102
|
+
<% end %>
|
|
103
|
+
<% end %>
|
|
104
|
+
|
|
105
|
+
<% if block_given? %>
|
|
106
|
+
<div class="navbar-custom-content inline-flex items-center">
|
|
107
|
+
<%= yield %>
|
|
108
|
+
</div>
|
|
109
|
+
<% end %>
|
|
110
|
+
</nav>
|
|
111
|
+
|
|
112
|
+
<%# Hidden data element for JavaScript to access available themes %>
|
|
113
|
+
<div data-available-themes='<%= (NeonSakura.config.available_themes || []).to_json %>' style="display: none;"></div>
|
|
114
|
+
|
|
115
|
+
<%# Optional: Hidden element for API endpoint configuration %>
|
|
116
|
+
<% if NeonSakura.config.theme_api_endpoint.present? %>
|
|
117
|
+
<div data-theme-api-endpoint="<%= NeonSakura.config.theme_api_endpoint %>" style="display: none;"></div>
|
|
118
|
+
<% end %>
|
|
119
|
+
|
|
120
|
+
<script>
|
|
121
|
+
(function() {
|
|
122
|
+
'use strict';
|
|
123
|
+
|
|
124
|
+
// Store global handlers flag on window to persist across page loads
|
|
125
|
+
window.neonSakuraNavbar = window.neonSakuraNavbar || {
|
|
126
|
+
globalHandlersSet: false
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
function handleOutsideClick(event) {
|
|
130
|
+
if (!event.target.closest('[data-navbar-dropdown]')) {
|
|
131
|
+
closeAllNavDropdowns();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function handleEscapeKey(event) {
|
|
136
|
+
if (event.key === 'Escape') {
|
|
137
|
+
closeAllNavDropdowns();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function initializeNavbarDropdowns() {
|
|
142
|
+
const dropdowns = document.querySelectorAll('[data-navbar-dropdown]');
|
|
143
|
+
|
|
144
|
+
dropdowns.forEach(function(dropdown) {
|
|
145
|
+
const toggle = dropdown.querySelector('[data-action="toggle"]');
|
|
146
|
+
const menu = dropdown.querySelector('[data-menu]');
|
|
147
|
+
|
|
148
|
+
if (!toggle || !menu) return;
|
|
149
|
+
|
|
150
|
+
// Remove any existing listeners to prevent duplicates
|
|
151
|
+
const newToggle = toggle.cloneNode(true);
|
|
152
|
+
toggle.parentNode.replaceChild(newToggle, toggle);
|
|
153
|
+
|
|
154
|
+
// Add click handler for toggle button
|
|
155
|
+
newToggle.addEventListener('click', function(event) {
|
|
156
|
+
event.preventDefault();
|
|
157
|
+
event.stopPropagation();
|
|
158
|
+
|
|
159
|
+
const isOpen = dropdown.classList.contains('open');
|
|
160
|
+
|
|
161
|
+
// Close all other dropdowns first
|
|
162
|
+
closeAllNavDropdowns();
|
|
163
|
+
|
|
164
|
+
// Toggle this dropdown
|
|
165
|
+
if (!isOpen) {
|
|
166
|
+
dropdown.classList.add('open');
|
|
167
|
+
newToggle.setAttribute('aria-expanded', 'true');
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Set up global handlers only once
|
|
173
|
+
if (!window.neonSakuraNavbar.globalHandlersSet) {
|
|
174
|
+
document.addEventListener('click', handleOutsideClick);
|
|
175
|
+
document.addEventListener('keydown', handleEscapeKey);
|
|
176
|
+
window.neonSakuraNavbar.globalHandlersSet = true;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function closeAllNavDropdowns() {
|
|
181
|
+
const openDropdowns = document.querySelectorAll('[data-navbar-dropdown].open');
|
|
182
|
+
|
|
183
|
+
openDropdowns.forEach(function(dropdown) {
|
|
184
|
+
dropdown.classList.remove('open');
|
|
185
|
+
const toggle = dropdown.querySelector('[data-action="toggle"]');
|
|
186
|
+
if (toggle) {
|
|
187
|
+
toggle.setAttribute('aria-expanded', 'false');
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Initialize on DOMContentLoaded
|
|
193
|
+
if (document.readyState === 'loading') {
|
|
194
|
+
document.addEventListener('DOMContentLoaded', initializeNavbarDropdowns);
|
|
195
|
+
} else {
|
|
196
|
+
initializeNavbarDropdowns();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Re-initialize on Turbo page loads
|
|
200
|
+
if (typeof Turbo !== 'undefined') {
|
|
201
|
+
document.addEventListener('turbo:load', initializeNavbarDropdowns);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Re-initialize on Turbolinks page loads (legacy)
|
|
205
|
+
document.addEventListener('turbolinks:load', initializeNavbarDropdowns);
|
|
206
|
+
|
|
207
|
+
// Theme switcher functionality
|
|
208
|
+
const STORAGE_KEY_NAME = 'neon_sakura_theme_name';
|
|
209
|
+
const STORAGE_KEY_MODE = 'neon_sakura_theme_mode';
|
|
210
|
+
|
|
211
|
+
function getAvailableThemes() {
|
|
212
|
+
const themesElement = document.querySelector('[data-available-themes]');
|
|
213
|
+
if (themesElement) {
|
|
214
|
+
try {
|
|
215
|
+
return JSON.parse(themesElement.dataset.availableThemes);
|
|
216
|
+
} catch (e) {
|
|
217
|
+
console.error('Failed to parse available themes:', e);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return [{ name: 'purple', mode: 'dark', label: 'Purple Dark' }];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function getCurrentTheme() {
|
|
224
|
+
const themes = getAvailableThemes();
|
|
225
|
+
const storedName = localStorage.getItem(STORAGE_KEY_NAME);
|
|
226
|
+
const storedMode = localStorage.getItem(STORAGE_KEY_MODE);
|
|
227
|
+
|
|
228
|
+
if (storedName && storedMode) {
|
|
229
|
+
const found = themes.find(function(t) { return t.name === storedName && t.mode === storedMode; });
|
|
230
|
+
if (found) return found;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return themes[0];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function applyTheme(theme) {
|
|
237
|
+
const html = document.documentElement;
|
|
238
|
+
html.setAttribute('data-theme-name', theme.name);
|
|
239
|
+
html.setAttribute('data-theme-mode', theme.mode);
|
|
240
|
+
|
|
241
|
+
localStorage.setItem(STORAGE_KEY_NAME, theme.name);
|
|
242
|
+
localStorage.setItem(STORAGE_KEY_MODE, theme.mode);
|
|
243
|
+
|
|
244
|
+
document.cookie = 'theme_name=' + theme.name + '; path=/; max-age=31536000';
|
|
245
|
+
document.cookie = 'theme_mode=' + theme.mode + '; path=/; max-age=31536000';
|
|
246
|
+
|
|
247
|
+
updateThemeUI(theme);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function updateThemeUI(currentTheme) {
|
|
251
|
+
const themeItems = document.querySelectorAll('[data-theme-item]');
|
|
252
|
+
themeItems.forEach(function(item) {
|
|
253
|
+
const themeName = item.dataset.themeName;
|
|
254
|
+
const themeMode = item.dataset.themeMode;
|
|
255
|
+
const isActive = themeName === currentTheme.name && themeMode === currentTheme.mode;
|
|
256
|
+
|
|
257
|
+
const checkmark = item.querySelector('[data-theme-checkmark]');
|
|
258
|
+
if (checkmark) {
|
|
259
|
+
checkmark.style.display = isActive ? 'inline' : 'none';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (isActive) {
|
|
263
|
+
item.setAttribute('aria-current', 'true');
|
|
264
|
+
} else {
|
|
265
|
+
item.removeAttribute('aria-current');
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function initializeThemeSwitcher() {
|
|
271
|
+
const currentTheme = getCurrentTheme();
|
|
272
|
+
applyTheme(currentTheme);
|
|
273
|
+
|
|
274
|
+
const themeItems = document.querySelectorAll('[data-theme-item]');
|
|
275
|
+
themeItems.forEach(function(item) {
|
|
276
|
+
// Remove existing listener
|
|
277
|
+
const newItem = item.cloneNode(true);
|
|
278
|
+
item.parentNode.replaceChild(newItem, item);
|
|
279
|
+
|
|
280
|
+
newItem.addEventListener('click', function(event) {
|
|
281
|
+
event.preventDefault();
|
|
282
|
+
const themeName = newItem.dataset.themeName;
|
|
283
|
+
const themeMode = newItem.dataset.themeMode;
|
|
284
|
+
const theme = getAvailableThemes().find(function(t) { return t.name === themeName && t.mode === themeMode; });
|
|
285
|
+
if (theme) {
|
|
286
|
+
applyTheme(theme);
|
|
287
|
+
closeAllNavDropdowns();
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Initialize theme switcher after dropdowns
|
|
294
|
+
if (document.readyState === 'loading') {
|
|
295
|
+
document.addEventListener('DOMContentLoaded', initializeThemeSwitcher);
|
|
296
|
+
} else {
|
|
297
|
+
initializeThemeSwitcher();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (typeof Turbo !== 'undefined') {
|
|
301
|
+
document.addEventListener('turbo:load', initializeThemeSwitcher);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
document.addEventListener('turbolinks:load', initializeThemeSwitcher);
|
|
305
|
+
})();
|
|
306
|
+
</script>
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
<%
|
|
2
|
+
# Profile Image Selector Component
|
|
3
|
+
# Radio card selection UI for choosing profile image source
|
|
4
|
+
#
|
|
5
|
+
# Parameters:
|
|
6
|
+
# user: User object with profile_image_source attribute
|
|
7
|
+
# form: Form builder object (from form_with)
|
|
8
|
+
# field_name: Name of the field (default: :profile_image_source)
|
|
9
|
+
# sources: Array of image source options (default: [:default, :gravatar, :upload])
|
|
10
|
+
# show_upload: Show upload option (default: false, requires ActiveStorage)
|
|
11
|
+
|
|
12
|
+
user = local_assigns[:user]
|
|
13
|
+
form = local_assigns[:form]
|
|
14
|
+
field_name = local_assigns[:field_name] || :profile_image_source
|
|
15
|
+
sources = local_assigns[:sources] || [:default, :gravatar]
|
|
16
|
+
show_upload = local_assigns[:show_upload] || false
|
|
17
|
+
%>
|
|
18
|
+
|
|
19
|
+
<div class="profile-image-selector">
|
|
20
|
+
<label class="form-label font-medium"><%= local_assigns[:label] || "Profile Image Source" %></label>
|
|
21
|
+
<div class="profile-image-options">
|
|
22
|
+
<% if sources.include?(:default) %>
|
|
23
|
+
<div class="profile-image-option" data-value="">
|
|
24
|
+
<%= form.radio_button field_name, "",
|
|
25
|
+
id: "#{user.class.name.underscore}_#{field_name}_default",
|
|
26
|
+
checked: user.send(field_name).blank?,
|
|
27
|
+
class: "profile-image-radio d-none" %>
|
|
28
|
+
<div class="profile-image-card <%= 'selected' if user.send(field_name).blank? %>">
|
|
29
|
+
<div class="profile-image-card-body">
|
|
30
|
+
<% if user.respond_to?(:default_avatar_url) %>
|
|
31
|
+
<img src="<%= user.default_avatar_url %>" alt="Default Avatar" class="profile-image-preview">
|
|
32
|
+
<% else %>
|
|
33
|
+
<%= render "shared/icons/user", css_class: "w-15 h-15 text-text-muted" %>
|
|
34
|
+
<% end %>
|
|
35
|
+
<h6 class="profile-image-card-title">Default</h6>
|
|
36
|
+
<p class="profile-image-card-text">Generated avatar</p>
|
|
37
|
+
<div class="profile-image-selection-indicator">
|
|
38
|
+
<%= render "shared/icons/check_circle", css_class: "w-5 h-5 text-accent" %>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
<% end %>
|
|
44
|
+
|
|
45
|
+
<% if sources.include?(:gravatar) %>
|
|
46
|
+
<div class="profile-image-option" data-value="gravatar">
|
|
47
|
+
<%= form.radio_button field_name, "gravatar",
|
|
48
|
+
id: "#{user.class.name.underscore}_#{field_name}_gravatar",
|
|
49
|
+
checked: user.send(field_name) == 'gravatar',
|
|
50
|
+
class: "profile-image-radio d-none" %>
|
|
51
|
+
<div class="profile-image-card <%= 'selected' if user.send(field_name) == 'gravatar' %>">
|
|
52
|
+
<div class="profile-image-card-body">
|
|
53
|
+
<% if user.respond_to?(:email) %>
|
|
54
|
+
<img src="<%= gravatar_url(user.email, size: 60) %>" alt="Gravatar" class="profile-image-preview">
|
|
55
|
+
<% else %>
|
|
56
|
+
<%= render "shared/icons/user_circle", css_class: "w-15 h-15 text-text-muted" %>
|
|
57
|
+
<% end %>
|
|
58
|
+
<h6 class="profile-image-card-title">
|
|
59
|
+
<%= render "shared/icons/user_circle", css_class: "w-4 h-4 inline-block mr-1" %>
|
|
60
|
+
Gravatar
|
|
61
|
+
</h6>
|
|
62
|
+
<p class="profile-image-card-text">Use your Gravatar image</p>
|
|
63
|
+
<div class="profile-image-selection-indicator">
|
|
64
|
+
<%= render "shared/icons/check_circle", css_class: "w-5 h-5 text-accent" %>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
<% end %>
|
|
70
|
+
|
|
71
|
+
<% if show_upload && sources.include?(:upload) %>
|
|
72
|
+
<div class="profile-image-option" data-value="upload">
|
|
73
|
+
<%= form.radio_button field_name, "upload",
|
|
74
|
+
id: "#{user.class.name.underscore}_#{field_name}_upload",
|
|
75
|
+
checked: user.send(field_name) == 'upload',
|
|
76
|
+
class: "profile-image-radio d-none" %>
|
|
77
|
+
<div class="profile-image-card <%= 'selected' if user.send(field_name) == 'upload' %>">
|
|
78
|
+
<div class="profile-image-card-body">
|
|
79
|
+
<% if user.respond_to?(:profile_picture) && user.profile_picture.attached? %>
|
|
80
|
+
<img src="<%= url_for(user.profile_picture.variant(:thumbnail)) %>" alt="Uploaded Picture" class="profile-image-preview">
|
|
81
|
+
<% else %>
|
|
82
|
+
<%= render "shared/icons/upload", css_class: "w-15 h-15 text-text-muted" %>
|
|
83
|
+
<% end %>
|
|
84
|
+
<h6 class="profile-image-card-title">
|
|
85
|
+
<%= render "shared/icons/upload", css_class: "w-4 h-4 inline-block mr-1" %>
|
|
86
|
+
Upload
|
|
87
|
+
</h6>
|
|
88
|
+
<p class="profile-image-card-text">Upload your own image</p>
|
|
89
|
+
<div class="profile-image-selection-indicator">
|
|
90
|
+
<%= render "shared/icons/check_circle", css_class: "w-5 h-5 text-accent" %>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
<% end %>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<% if sources.include?(:gravatar) %>
|
|
99
|
+
<div class="form-text mt-2">
|
|
100
|
+
<strong>Gravatar:</strong> Create an account at
|
|
101
|
+
<a href="https://gravatar.com" target="_blank" rel="noopener noreferrer" class="text-decoration-none">gravatar.com</a>
|
|
102
|
+
using <%= user.respond_to?(:email) ? 'your email address' : 'this email address' %> to set your profile image.
|
|
103
|
+
</div>
|
|
104
|
+
<% end %>
|
|
105
|
+
|
|
106
|
+
<% if show_upload && sources.include?(:upload) %>
|
|
107
|
+
<div id="profile-picture-upload-zone" class="mt-4 <%= 'hidden' unless user.send(field_name) == 'upload' %>">
|
|
108
|
+
<%= render "shared/file_upload",
|
|
109
|
+
field_name: "#{user.class.name.underscore}[profile_picture]",
|
|
110
|
+
accept: "image/*",
|
|
111
|
+
max_size: 5,
|
|
112
|
+
preview: true,
|
|
113
|
+
css_class: "profile-picture-upload" %>
|
|
114
|
+
</div>
|
|
115
|
+
<% end %>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<script>
|
|
119
|
+
// Profile image selector JavaScript
|
|
120
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
121
|
+
setupProfileImageSelector();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
document.addEventListener('turbo:load', function() {
|
|
125
|
+
setupProfileImageSelector();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
function setupProfileImageSelector() {
|
|
129
|
+
const profileOptions = document.querySelectorAll('.profile-image-option');
|
|
130
|
+
const uploadZone = document.getElementById('profile-picture-upload-zone');
|
|
131
|
+
|
|
132
|
+
profileOptions.forEach(option => {
|
|
133
|
+
option.addEventListener('click', function() {
|
|
134
|
+
const value = this.dataset.value;
|
|
135
|
+
const radioButton = this.querySelector('input[type="radio"]');
|
|
136
|
+
|
|
137
|
+
if (radioButton) {
|
|
138
|
+
// Uncheck all radio buttons and remove selected class
|
|
139
|
+
profileOptions.forEach(opt => {
|
|
140
|
+
const radio = opt.querySelector('input[type="radio"]');
|
|
141
|
+
const card = opt.querySelector('.profile-image-card');
|
|
142
|
+
if (radio) radio.checked = false;
|
|
143
|
+
if (card) card.classList.remove('selected');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Check the clicked radio button and add selected class
|
|
147
|
+
radioButton.checked = true;
|
|
148
|
+
const card = this.querySelector('.profile-image-card');
|
|
149
|
+
if (card) {
|
|
150
|
+
card.classList.add('selected');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Show/hide upload zone
|
|
154
|
+
if (uploadZone) {
|
|
155
|
+
if (value === 'upload') {
|
|
156
|
+
uploadZone.classList.remove('hidden');
|
|
157
|
+
} else {
|
|
158
|
+
uploadZone.classList.add('hidden');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
</script>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<%
|
|
2
|
+
# Theme switcher component
|
|
3
|
+
# Supports 3 modes based on available theme count:
|
|
4
|
+
# - disabled (1 theme): Shows current theme, button disabled
|
|
5
|
+
# - toggle (2 themes): Simple moon/sun toggle button
|
|
6
|
+
# - dropdown (3+ themes): Dropdown with checkmarks
|
|
7
|
+
|
|
8
|
+
themes = NeonSakura.config.available_themes || []
|
|
9
|
+
theme_count = themes.length
|
|
10
|
+
mode = theme_count <= 1 ? :disabled : (theme_count == 2 ? :toggle : :dropdown)
|
|
11
|
+
%>
|
|
12
|
+
|
|
13
|
+
<% if mode == :disabled %>
|
|
14
|
+
<%# Single theme - disabled button showing current theme %>
|
|
15
|
+
<button class="nav-theme-button disabled" disabled aria-label="Theme: <%= themes.first[:label] %>">
|
|
16
|
+
<%= render_theme_icon("palette", css_class: "w-4 h-4") %>
|
|
17
|
+
<span class="sr-only"><%= themes.first[:label] %></span>
|
|
18
|
+
</button>
|
|
19
|
+
|
|
20
|
+
<% elsif mode == :toggle %>
|
|
21
|
+
<%# Two themes - simple toggle button %>
|
|
22
|
+
<button id="theme-toggle" class="nav-theme-button" aria-label="Toggle theme">
|
|
23
|
+
<span id="theme-icon">
|
|
24
|
+
<%= render_theme_icon("moon", css_class: "w-4 h-4") %>
|
|
25
|
+
</span>
|
|
26
|
+
</button>
|
|
27
|
+
|
|
28
|
+
<% else %>
|
|
29
|
+
<%# Three or more themes - dropdown menu %>
|
|
30
|
+
<div class="nav-dropdown" data-dropdown>
|
|
31
|
+
<button class="nav-dropdown-toggle" data-action="toggle" aria-label="Theme menu" aria-haspopup="true" aria-expanded="false">
|
|
32
|
+
<%= render_theme_icon("palette", css_class: "w-4 h-4") %>
|
|
33
|
+
<span class="hidden sm:inline">Theme</span>
|
|
34
|
+
<%= render_theme_icon("chevron_down", css_class: "w-4 h-4") %>
|
|
35
|
+
</button>
|
|
36
|
+
<div class="nav-dropdown-menu" data-menu>
|
|
37
|
+
<% themes.each do |theme| %>
|
|
38
|
+
<a href="#"
|
|
39
|
+
data-theme-item
|
|
40
|
+
data-theme-name="<%= theme[:name] %>"
|
|
41
|
+
data-theme-mode="<%= theme[:mode] %>"
|
|
42
|
+
aria-label="Switch to <%= theme[:label] %>">
|
|
43
|
+
<span data-theme-checkmark style="display: none;">
|
|
44
|
+
<%= render_theme_icon("check", css_class: "w-4 h-4") %>
|
|
45
|
+
</span>
|
|
46
|
+
<% if theme[:mode] == 'light' %>
|
|
47
|
+
<%= render_theme_icon("sun", css_class: "w-4 h-4") %>
|
|
48
|
+
<% else %>
|
|
49
|
+
<%= render_theme_icon("moon", css_class: "w-4 h-4") %>
|
|
50
|
+
<% end %>
|
|
51
|
+
<%= theme[:label] %>
|
|
52
|
+
</a>
|
|
53
|
+
<% end %>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
<% end %>
|
|
57
|
+
|
|
58
|
+
<%# Hidden data element for JavaScript to access available themes %>
|
|
59
|
+
<div data-available-themes='<%= themes.to_json %>' style="display: none;"></div>
|
|
60
|
+
|
|
61
|
+
<%# Optional: Hidden element for API endpoint configuration %>
|
|
62
|
+
<% if NeonSakura.config.theme_api_endpoint.present? %>
|
|
63
|
+
<div data-theme-api-endpoint="<%= NeonSakura.config.theme_api_endpoint %>" style="display: none;"></div>
|
|
64
|
+
<% end %>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<%# Icon: Adjustments (sliders/settings) %>
|
|
2
|
+
<%# Source: https://heroicons.com/ %>
|
|
3
|
+
<svg xmlns="http://www.w3.org/2000/svg"
|
|
4
|
+
fill="none"
|
|
5
|
+
viewBox="0 0 24 24"
|
|
6
|
+
stroke-width="1.5"
|
|
7
|
+
stroke="currentColor"
|
|
8
|
+
class="<%= css_class %>">
|
|
9
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 6h9.75M10.5 6a1.5 1.5 0 1 1-3 0m3 0a1.5 1.5 0 1 0-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-9.75 0h9.75" />
|
|
10
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg class="<%= local_assigns[:css_class] || 'w-4 h-4' %>" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="<%= local_assigns[:aria_hidden] || 'true' %>">
|
|
2
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg class="<%= local_assigns[:css_class] || 'w-4 h-4' %>" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="<%= local_assigns[:aria_hidden] || 'true' %>">
|
|
2
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg class="<%= local_assigns[:css_class] || 'w-4 h-4' %>" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="<%= local_assigns[:aria_hidden] || 'true' %>">
|
|
2
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"></path>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg class="<%= local_assigns[:css_class] || 'w-4 h-4' %>" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="<%= local_assigns[:aria_hidden] || 'true' %>">
|
|
2
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 13l-5 5m0 0l-5-5m5 5V6"></path>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg class="<%= local_assigns[:css_class] || 'w-4 h-4' %>" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="<%= local_assigns[:aria_hidden] || 'true' %>">
|
|
2
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 17l-5-5m0 0l5-5m-5 5h12"></path>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg class="<%= local_assigns[:css_class] || 'w-4 h-4' %>" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="<%= local_assigns[:aria_hidden] || 'true' %>">
|
|
2
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 11l5-5m0 0l5 5m-5-5v12"></path>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<%# Icon: Arrows Pointing In (compress, reset zoom) %>
|
|
2
|
+
<%# Source: https://heroicons.com/ %>
|
|
3
|
+
<svg xmlns="http://www.w3.org/2000/svg"
|
|
4
|
+
fill="none"
|
|
5
|
+
viewBox="0 0 24 24"
|
|
6
|
+
stroke-width="1.5"
|
|
7
|
+
stroke="currentColor"
|
|
8
|
+
class="<%= css_class %>">
|
|
9
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M9 9V4.5M9 9H4.5M9 9L3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5m0-4.5l5.25 5.25" />
|
|
10
|
+
</svg>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<%# Icon: Arrows Pointing Out (expand/fullscreen) %>
|
|
2
|
+
<%# Source: https://heroicons.com/ %>
|
|
3
|
+
<svg xmlns="http://www.w3.org/2000/svg"
|
|
4
|
+
fill="none"
|
|
5
|
+
viewBox="0 0 24 24"
|
|
6
|
+
stroke-width="1.5"
|
|
7
|
+
stroke="currentColor"
|
|
8
|
+
class="<%= css_class %>">
|
|
9
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15" />
|
|
10
|
+
</svg>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<%
|
|
2
|
+
# Accept gradient_id parameter to allow multiple logos on same page
|
|
3
|
+
gradient_id = local_assigns[:gradient_id] || "purpleGradient"
|
|
4
|
+
css_class = local_assigns[:css_class] || ""
|
|
5
|
+
%>
|
|
6
|
+
<svg viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg" class="<%= css_class %>" aria-hidden="<%= local_assigns[:aria_hidden] || 'true' %>" role="<%= local_assigns[:role] || 'img' %>">
|
|
7
|
+
<% if local_assigns[:title] %>
|
|
8
|
+
<title><%= local_assigns[:title] %></title>
|
|
9
|
+
<% end %>
|
|
10
|
+
<defs>
|
|
11
|
+
<linearGradient id="<%= gradient_id %>" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
12
|
+
<stop offset="0%" style="stop-color:#6B46C1;stop-opacity:1" />
|
|
13
|
+
<stop offset="50%" style="stop-color:#7C3AED;stop-opacity:1" />
|
|
14
|
+
<stop offset="100%" style="stop-color:#4C1D95;stop-opacity:1" />
|
|
15
|
+
</linearGradient>
|
|
16
|
+
</defs>
|
|
17
|
+
<circle cx="30" cy="30" r="30" fill="url(#<%= gradient_id %>)"/>
|
|
18
|
+
<circle cx="25.8" cy="25.8" r="14.1" fill="none" stroke="#7DD3FC" stroke-width="2.8" stroke-linecap="round"/>
|
|
19
|
+
<line x1="36.3" y1="36.3" x2="44.55" y2="44.55" stroke="#7DD3FC" stroke-width="2.8" stroke-linecap="round"/>
|
|
20
|
+
<g transform="translate(25.8, 25.8)">
|
|
21
|
+
<circle cx="1.76" cy="0" r="5.85" fill="#7DD3FC"/>
|
|
22
|
+
<circle cx="3.52" cy="0" r="5.85" fill="url(#<%= gradient_id %>)"/>
|
|
23
|
+
</g>
|
|
24
|
+
<circle cx="21.07" cy="18.75" r="0.94" fill="#7DD3FC" opacity="0.6"/>
|
|
25
|
+
<circle cx="30.45" cy="21.07" r="0.7" fill="#7DD3FC" opacity="0.4"/>
|
|
26
|
+
</svg>
|