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,416 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" <%= theme_data_attributes %>>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<title><%= @page_title || "Style Guide" %> | Neon Sakura</title>
|
|
7
|
+
|
|
8
|
+
<!-- Favicon: Palette icon -->
|
|
9
|
+
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23a855f7' stroke-width='1.5'><path stroke-linecap='round' stroke-linejoin='round' d='M4.098 19.902a3.75 3.75 0 0 0 5.304 0l6.401-6.402M6.75 21A3.75 3.75 0 0 1 3 17.25V4.125C3 3.504 3.504 3 4.125 3h5.25c.621 0 1.125.504 1.125 1.125v4.072M6.75 21a3.75 3.75 0 0 0 3.75-3.75V8.197M6.75 21h13.125c.621 0 1.125-.504 1.125-1.125v-5.25c0-.621-.504-1.125-1.125-1.125h-4.072M10.5 8.197l2.88-2.88c.438-.439 1.15-.439 1.59 0l3.712 3.713c.44.44.44 1.152 0 1.59l-2.879 2.88M6.75 17.25h.008v.008H6.75v-.008Z'/></svg>">
|
|
10
|
+
|
|
11
|
+
<%= neon_sakura_stylesheets %>
|
|
12
|
+
|
|
13
|
+
<!-- Pagy JavaScript for interactive pagination -->
|
|
14
|
+
<% if defined?(Pagy) %>
|
|
15
|
+
<script src="https://unpkg.com/pagy@43/javascripts/pagy.min.js"></script>
|
|
16
|
+
<% end %>
|
|
17
|
+
|
|
18
|
+
<style>
|
|
19
|
+
/* Inline styles for code examples display */
|
|
20
|
+
.code-example-wrapper {
|
|
21
|
+
margin: 1.5rem 0;
|
|
22
|
+
border: 1px solid var(--color-border);
|
|
23
|
+
border-radius: 0.5rem;
|
|
24
|
+
overflow: hidden;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.code-example-preview {
|
|
28
|
+
padding: 2rem;
|
|
29
|
+
background-color: var(--color-surface);
|
|
30
|
+
border-bottom: 1px solid var(--color-border);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.code-example-code {
|
|
34
|
+
background-color: var(--color-background);
|
|
35
|
+
padding: 1rem;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.code-example-code pre {
|
|
39
|
+
margin: 0;
|
|
40
|
+
overflow-x: auto;
|
|
41
|
+
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
|
42
|
+
font-size: 0.875rem;
|
|
43
|
+
line-height: 1.5;
|
|
44
|
+
color: var(--color-text-secondary);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.section-divider {
|
|
48
|
+
margin: 3rem 0;
|
|
49
|
+
border-top: 2px solid var(--color-border);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.sticky-header {
|
|
53
|
+
position: sticky;
|
|
54
|
+
top: 0;
|
|
55
|
+
z-index: 10;
|
|
56
|
+
background-color: var(--color-background);
|
|
57
|
+
border-bottom: 1px solid var(--color-border);
|
|
58
|
+
padding-bottom: 0.2rem;
|
|
59
|
+
margin-bottom: 1.5rem;
|
|
60
|
+
}
|
|
61
|
+
</style>
|
|
62
|
+
</head>
|
|
63
|
+
|
|
64
|
+
<body class="bg-background text-text-primary min-h-screen">
|
|
65
|
+
<!-- Sticky Navbar (always sticky for style guide) -->
|
|
66
|
+
<nav class="sticky-header top-0 z-50 bg-surface border-b border-border shadow-lg">
|
|
67
|
+
<div class="container mx-auto px-4 max-w-7xl">
|
|
68
|
+
<div class="flex items-center justify-between py-2">
|
|
69
|
+
<!-- Left: Back link and title -->
|
|
70
|
+
<div class="flex items-center gap-3">
|
|
71
|
+
<%= link_to root_path, class: "text-white hover:text-accent transition-colors flex items-center gap-2 px-2 py-1 rounded hover:bg-background" do %>
|
|
72
|
+
<%= render_theme_icon("arrow_left", css_class: "w-4 h-4") %>
|
|
73
|
+
<span class="text-sm font-medium">Back</span>
|
|
74
|
+
<% end %>
|
|
75
|
+
<span class="text-border">|</span>
|
|
76
|
+
<h1 class="text-base font-bold bg-gradient-to-r from-accent to-notification bg-clip-text text-transparent">
|
|
77
|
+
Style Guide
|
|
78
|
+
</h1>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<!-- Right: Compact theme selector -->
|
|
82
|
+
<div class="flex items-center gap-2">
|
|
83
|
+
<% @available_themes.each do |theme| %>
|
|
84
|
+
<%
|
|
85
|
+
is_current = @current_theme[:name] == theme[:name] && @current_theme[:mode] == theme[:mode]
|
|
86
|
+
%>
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
data-theme-item
|
|
90
|
+
data-theme-name="<%= theme[:name] %>"
|
|
91
|
+
data-theme-mode="<%= theme[:mode] %>"
|
|
92
|
+
class="px-2 py-1 text-xs rounded border transition-all <%= is_current ? 'border-accent bg-accent font-semibold' : 'border-border bg-surface hover:border-accent' %>"
|
|
93
|
+
style="<%= is_current ? 'color: white;' : 'color: var(--color-text-primary);' %>"
|
|
94
|
+
aria-label="Switch to <%= theme[:label] %>"
|
|
95
|
+
aria-pressed="<%= is_current %>"
|
|
96
|
+
title="<%= theme[:label] %>"
|
|
97
|
+
>
|
|
98
|
+
<% if theme[:mode] == 'light' %>
|
|
99
|
+
<%= render_theme_icon("sun", css_class: "w-3 h-3") %>
|
|
100
|
+
<% else %>
|
|
101
|
+
<%= render_theme_icon("moon", css_class: "w-3 h-3") %>
|
|
102
|
+
<% end %>
|
|
103
|
+
</button>
|
|
104
|
+
<% end %>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</nav>
|
|
109
|
+
|
|
110
|
+
<script>
|
|
111
|
+
// Navbar theme switcher
|
|
112
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
113
|
+
const buttons = document.querySelectorAll('nav [data-theme-item]');
|
|
114
|
+
|
|
115
|
+
buttons.forEach(button => {
|
|
116
|
+
button.addEventListener('click', function() {
|
|
117
|
+
const themeName = this.dataset.themeName;
|
|
118
|
+
const themeMode = this.dataset.themeMode;
|
|
119
|
+
|
|
120
|
+
document.documentElement.setAttribute('data-theme-name', themeName);
|
|
121
|
+
document.documentElement.setAttribute('data-theme-mode', themeMode);
|
|
122
|
+
localStorage.setItem('neon_sakura_theme_name', themeName);
|
|
123
|
+
localStorage.setItem('neon_sakura_theme_mode', themeMode);
|
|
124
|
+
|
|
125
|
+
buttons.forEach(btn => {
|
|
126
|
+
const isActive = btn.dataset.themeName === themeName && btn.dataset.themeMode === themeMode;
|
|
127
|
+
if (isActive) {
|
|
128
|
+
btn.classList.add('border-accent', 'bg-accent', 'font-semibold');
|
|
129
|
+
btn.classList.remove('border-border', 'bg-surface', 'hover:border-accent');
|
|
130
|
+
btn.style.color = 'white';
|
|
131
|
+
} else {
|
|
132
|
+
btn.classList.remove('border-accent', 'bg-accent', 'font-semibold');
|
|
133
|
+
btn.classList.add('border-border', 'bg-surface', 'hover:border-accent');
|
|
134
|
+
btn.style.color = 'var(--color-text-primary)';
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
</script>
|
|
141
|
+
|
|
142
|
+
<!-- Main Content -->
|
|
143
|
+
<div class="container mx-auto px-4 py-8 max-w-7xl">
|
|
144
|
+
<%= yield %>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<!-- Modal for Icons and Images -->
|
|
148
|
+
<div id="zoom-modal" class="modal-overlay">
|
|
149
|
+
<div class="modal-backdrop"></div>
|
|
150
|
+
<div class="modal-container">
|
|
151
|
+
<button id="modal-close" class="modal-close-button" aria-label="Close modal">
|
|
152
|
+
<%= render_theme_icon("close", css_class: "w-6 h-6") %>
|
|
153
|
+
</button>
|
|
154
|
+
<!-- Zoom Controls -->
|
|
155
|
+
<div id="zoom-controls" class="zoom-controls">
|
|
156
|
+
<button id="zoom-in" class="zoom-control-button" aria-label="Zoom in" title="Zoom in">
|
|
157
|
+
<%= render_theme_icon("plus", css_class: "w-5 h-5") %>
|
|
158
|
+
</button>
|
|
159
|
+
<button id="zoom-out" class="zoom-control-button" aria-label="Zoom out" title="Zoom out">
|
|
160
|
+
<%= render_theme_icon("minus", css_class: "w-5 h-5") %>
|
|
161
|
+
</button>
|
|
162
|
+
<button id="zoom-reset" class="zoom-control-button" aria-label="Reset zoom" title="Reset zoom (100%)">
|
|
163
|
+
<%= render_theme_icon("arrows_pointing_in", css_class: "w-5 h-5") %>
|
|
164
|
+
</button>
|
|
165
|
+
<span id="zoom-level" class="zoom-level-text">100%</span>
|
|
166
|
+
</div>
|
|
167
|
+
<div id="modal-content" class="modal-content">
|
|
168
|
+
<!-- Content will be inserted here -->
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<script>
|
|
174
|
+
// Modal functionality for icons and images with zoom
|
|
175
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
176
|
+
const modal = document.getElementById('zoom-modal');
|
|
177
|
+
const modalContent = document.getElementById('modal-content');
|
|
178
|
+
const modalClose = document.getElementById('modal-close');
|
|
179
|
+
const backdrop = modal.querySelector('.modal-backdrop');
|
|
180
|
+
const zoomInBtn = document.getElementById('zoom-in');
|
|
181
|
+
const zoomOutBtn = document.getElementById('zoom-out');
|
|
182
|
+
const zoomResetBtn = document.getElementById('zoom-reset');
|
|
183
|
+
const zoomLevelText = document.getElementById('zoom-level');
|
|
184
|
+
|
|
185
|
+
let zoomLevel = 1;
|
|
186
|
+
let panX = 0;
|
|
187
|
+
let panY = 0;
|
|
188
|
+
let isDragging = false;
|
|
189
|
+
let dragStartX = 0;
|
|
190
|
+
let dragStartY = 0;
|
|
191
|
+
let zoomableElement = null;
|
|
192
|
+
|
|
193
|
+
const MIN_ZOOM = 0.5;
|
|
194
|
+
const MAX_ZOOM = 5;
|
|
195
|
+
const ZOOM_STEP = 0.25;
|
|
196
|
+
|
|
197
|
+
function updateZoom() {
|
|
198
|
+
if (!zoomableElement) return;
|
|
199
|
+
|
|
200
|
+
zoomableElement.style.transform = `translate(${panX}px, ${panY}px) scale(${zoomLevel})`;
|
|
201
|
+
zoomLevelText.textContent = Math.round(zoomLevel * 100) + '%';
|
|
202
|
+
|
|
203
|
+
// Disable/enable buttons based on zoom level
|
|
204
|
+
zoomInBtn.disabled = zoomLevel >= MAX_ZOOM;
|
|
205
|
+
zoomOutBtn.disabled = zoomLevel <= MIN_ZOOM;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function zoomIn() {
|
|
209
|
+
if (zoomLevel < MAX_ZOOM) {
|
|
210
|
+
zoomLevel = Math.min(MAX_ZOOM, zoomLevel + ZOOM_STEP);
|
|
211
|
+
updateZoom();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function zoomOut() {
|
|
216
|
+
if (zoomLevel > MIN_ZOOM) {
|
|
217
|
+
zoomLevel = Math.max(MIN_ZOOM, zoomLevel - ZOOM_STEP);
|
|
218
|
+
updateZoom();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function resetZoom() {
|
|
223
|
+
zoomLevel = 1;
|
|
224
|
+
panX = 0;
|
|
225
|
+
panY = 0;
|
|
226
|
+
updateZoom();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function openModal() {
|
|
230
|
+
modal.classList.add('modal-open');
|
|
231
|
+
document.body.style.overflow = 'hidden';
|
|
232
|
+
resetZoom();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function closeModal() {
|
|
236
|
+
modal.classList.remove('modal-open');
|
|
237
|
+
document.body.style.overflow = '';
|
|
238
|
+
resetZoom();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Icon modal click handlers
|
|
242
|
+
document.querySelectorAll('[data-icon-modal]').forEach(iconCard => {
|
|
243
|
+
iconCard.addEventListener('click', function() {
|
|
244
|
+
const iconName = this.dataset.iconName;
|
|
245
|
+
|
|
246
|
+
// Find the SVG element in the clicked card
|
|
247
|
+
const svgElement = this.querySelector('svg');
|
|
248
|
+
if (!svgElement) {
|
|
249
|
+
console.error('No SVG found for icon:', iconName);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Clone the SVG and update it for modal display
|
|
254
|
+
const clonedSvg = svgElement.cloneNode(true);
|
|
255
|
+
|
|
256
|
+
// Set explicit dimensions for visibility
|
|
257
|
+
clonedSvg.style.width = '256px';
|
|
258
|
+
clonedSvg.style.height = '256px';
|
|
259
|
+
clonedSvg.style.display = 'block';
|
|
260
|
+
|
|
261
|
+
// Preserve color classes but remove size classes
|
|
262
|
+
const classList = clonedSvg.className.baseVal || clonedSvg.getAttribute('class') || '';
|
|
263
|
+
const newClasses = classList
|
|
264
|
+
.split(' ')
|
|
265
|
+
.filter(c => !c.match(/^(w-|h-)/)) // Remove size classes
|
|
266
|
+
.join(' ');
|
|
267
|
+
|
|
268
|
+
// Set the cleaned classes
|
|
269
|
+
if (clonedSvg.className.baseVal !== undefined) {
|
|
270
|
+
clonedSvg.className.baseVal = newClasses;
|
|
271
|
+
} else {
|
|
272
|
+
clonedSvg.setAttribute('class', newClasses);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Create the modal content
|
|
276
|
+
const modalWrapper = document.createElement('div');
|
|
277
|
+
modalWrapper.className = 'zoomable-wrapper';
|
|
278
|
+
modalWrapper.id = 'zoomable-wrapper';
|
|
279
|
+
|
|
280
|
+
const modalContentDiv = document.createElement('div');
|
|
281
|
+
modalContentDiv.className = 'zoomable-content';
|
|
282
|
+
modalContentDiv.id = 'zoomable-content';
|
|
283
|
+
|
|
284
|
+
const iconContainer = document.createElement('div');
|
|
285
|
+
iconContainer.className = 'flex justify-center items-center p-8 bg-surface rounded-lg border border-border';
|
|
286
|
+
iconContainer.style.minHeight = '300px';
|
|
287
|
+
iconContainer.appendChild(clonedSvg);
|
|
288
|
+
|
|
289
|
+
modalContentDiv.appendChild(iconContainer);
|
|
290
|
+
modalWrapper.appendChild(modalContentDiv);
|
|
291
|
+
|
|
292
|
+
// Clear and populate modal content
|
|
293
|
+
modalContent.innerHTML = '';
|
|
294
|
+
modalContent.appendChild(modalWrapper);
|
|
295
|
+
|
|
296
|
+
// Add icon name and code example
|
|
297
|
+
const heading = document.createElement('h3');
|
|
298
|
+
heading.className = 'text-2xl font-bold font-mono text-accent mt-4';
|
|
299
|
+
heading.textContent = iconName;
|
|
300
|
+
|
|
301
|
+
const codeBlock = document.createElement('code');
|
|
302
|
+
codeBlock.className = 'px-4 py-2 bg-surface border border-border rounded text-sm';
|
|
303
|
+
codeBlock.textContent = '<' + '%= render_theme_icon("' + iconName + '", css_class: "w-6 h-6") %' + '>';
|
|
304
|
+
|
|
305
|
+
modalContent.appendChild(heading);
|
|
306
|
+
modalContent.appendChild(codeBlock);
|
|
307
|
+
|
|
308
|
+
zoomableElement = document.getElementById('zoomable-content');
|
|
309
|
+
setupDragHandlers();
|
|
310
|
+
openModal();
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Image modal click handlers
|
|
315
|
+
document.querySelectorAll('[data-image-modal]').forEach(imageCard => {
|
|
316
|
+
imageCard.addEventListener('click', function() {
|
|
317
|
+
const imageSrc = this.dataset.imageSrc;
|
|
318
|
+
const imageName = this.dataset.imageName;
|
|
319
|
+
|
|
320
|
+
modalContent.innerHTML = `
|
|
321
|
+
<div class="zoomable-wrapper" id="zoomable-wrapper">
|
|
322
|
+
<div class="zoomable-content" id="zoomable-content">
|
|
323
|
+
<div class="flex justify-center items-center p-8 bg-surface rounded-lg border border-border">
|
|
324
|
+
<img src="${imageSrc}" alt="${imageName}" style="max-width: 100%; max-height: 60vh; width: auto; height: auto; object-fit: contain;">
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
<h3 class="text-2xl font-bold font-mono text-accent mt-4">${imageName}</h3>
|
|
329
|
+
<code class="px-4 py-2 bg-surface border border-border rounded text-sm">
|
|
330
|
+
<%= image_tag "${imageName}", class: "w-full" %>
|
|
331
|
+
</code>
|
|
332
|
+
`;
|
|
333
|
+
|
|
334
|
+
zoomableElement = document.getElementById('zoomable-content');
|
|
335
|
+
setupDragHandlers();
|
|
336
|
+
openModal();
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
function setupDragHandlers() {
|
|
341
|
+
const wrapper = document.getElementById('zoomable-wrapper');
|
|
342
|
+
if (!wrapper) return;
|
|
343
|
+
|
|
344
|
+
wrapper.addEventListener('mousedown', function(e) {
|
|
345
|
+
if (zoomLevel > 1) {
|
|
346
|
+
isDragging = true;
|
|
347
|
+
dragStartX = e.clientX - panX;
|
|
348
|
+
dragStartY = e.clientY - panY;
|
|
349
|
+
wrapper.classList.add('dragging');
|
|
350
|
+
e.preventDefault();
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
document.addEventListener('mousemove', function(e) {
|
|
355
|
+
if (isDragging) {
|
|
356
|
+
panX = e.clientX - dragStartX;
|
|
357
|
+
panY = e.clientY - dragStartY;
|
|
358
|
+
updateZoom();
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
document.addEventListener('mouseup', function() {
|
|
363
|
+
if (isDragging) {
|
|
364
|
+
isDragging = false;
|
|
365
|
+
const wrapper = document.getElementById('zoomable-wrapper');
|
|
366
|
+
if (wrapper) wrapper.classList.remove('dragging');
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Mouse wheel zoom
|
|
371
|
+
wrapper.addEventListener('wheel', function(e) {
|
|
372
|
+
e.preventDefault();
|
|
373
|
+
|
|
374
|
+
if (e.deltaY < 0) {
|
|
375
|
+
zoomIn();
|
|
376
|
+
} else {
|
|
377
|
+
zoomOut();
|
|
378
|
+
}
|
|
379
|
+
}, { passive: false });
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Zoom button handlers
|
|
383
|
+
zoomInBtn.addEventListener('click', zoomIn);
|
|
384
|
+
zoomOutBtn.addEventListener('click', zoomOut);
|
|
385
|
+
zoomResetBtn.addEventListener('click', resetZoom);
|
|
386
|
+
|
|
387
|
+
// Close modal handlers
|
|
388
|
+
modalClose.addEventListener('click', closeModal);
|
|
389
|
+
backdrop.addEventListener('click', closeModal);
|
|
390
|
+
|
|
391
|
+
// ESC key to close modal
|
|
392
|
+
document.addEventListener('keydown', function(e) {
|
|
393
|
+
if (e.key === 'Escape' && modal.classList.contains('modal-open')) {
|
|
394
|
+
closeModal();
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// Keyboard shortcuts for zoom
|
|
399
|
+
document.addEventListener('keydown', function(e) {
|
|
400
|
+
if (modal.classList.contains('modal-open')) {
|
|
401
|
+
if (e.key === '+' || e.key === '=') {
|
|
402
|
+
e.preventDefault();
|
|
403
|
+
zoomIn();
|
|
404
|
+
} else if (e.key === '-' || e.key === '_') {
|
|
405
|
+
e.preventDefault();
|
|
406
|
+
zoomOut();
|
|
407
|
+
} else if (e.key === '0') {
|
|
408
|
+
e.preventDefault();
|
|
409
|
+
resetZoom();
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
</script>
|
|
415
|
+
</body>
|
|
416
|
+
</html>
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
<%
|
|
2
|
+
# File Upload Component
|
|
3
|
+
# Provides drag-and-drop file upload with preview support
|
|
4
|
+
#
|
|
5
|
+
# Parameters:
|
|
6
|
+
# field_name: Name attribute for the file input (default: "file")
|
|
7
|
+
# accept: Accepted file types (default: "*/*")
|
|
8
|
+
# max_size: Maximum file size in MB (default: 10)
|
|
9
|
+
# preview: Show image preview (default: false)
|
|
10
|
+
# css_class: Additional CSS classes for container
|
|
11
|
+
# multiple: Allow multiple file selection (default: false)
|
|
12
|
+
|
|
13
|
+
field_name = local_assigns[:field_name] || "file"
|
|
14
|
+
accept = local_assigns[:accept] || "*/*"
|
|
15
|
+
max_size = local_assigns[:max_size] || 10
|
|
16
|
+
preview = local_assigns[:preview] || false
|
|
17
|
+
css_class = local_assigns[:css_class] || ""
|
|
18
|
+
multiple = local_assigns[:multiple] || false
|
|
19
|
+
unique_id = "file-upload-#{SecureRandom.hex(4)}"
|
|
20
|
+
%>
|
|
21
|
+
|
|
22
|
+
<div class="file-upload-container <%= css_class %>" data-controller="file-upload">
|
|
23
|
+
<div class="file-upload-zone" id="<%= unique_id %>-zone" data-file-upload-target="zone">
|
|
24
|
+
<input type="file"
|
|
25
|
+
name="<%= field_name %><%= multiple ? '[]' : '' %>"
|
|
26
|
+
id="<%= unique_id %>-input"
|
|
27
|
+
accept="<%= accept %>"
|
|
28
|
+
class="file-upload-input"
|
|
29
|
+
<%= 'multiple' if multiple %>
|
|
30
|
+
data-file-upload-target="input"
|
|
31
|
+
data-action="change->file-upload#handleFileSelect"
|
|
32
|
+
hidden>
|
|
33
|
+
|
|
34
|
+
<div class="file-upload-prompt" data-file-upload-target="prompt">
|
|
35
|
+
<%= render "shared/icons/upload", css_class: "w-12 h-12 mb-3 text-text-muted" %>
|
|
36
|
+
<p class="text-text-primary font-medium">Drag and drop <%= multiple ? 'files' : 'file' %> here</p>
|
|
37
|
+
<p class="text-text-muted text-sm">or</p>
|
|
38
|
+
<button type="button"
|
|
39
|
+
class="btn btn-outline-secondary mt-2"
|
|
40
|
+
onclick="document.getElementById('<%= unique_id %>-input').click()">
|
|
41
|
+
Browse Files
|
|
42
|
+
</button>
|
|
43
|
+
<p class="text-text-muted text-xs mt-3">Max size: <%= max_size %>MB<%= ' each' if multiple %></p>
|
|
44
|
+
<% if accept != "*/*" %>
|
|
45
|
+
<p class="text-text-muted text-xs">Accepted types: <%= accept %></p>
|
|
46
|
+
<% end %>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<% if preview %>
|
|
50
|
+
<div class="file-upload-preview-container hidden" data-file-upload-target="previewContainer">
|
|
51
|
+
<img src="" alt="Preview" class="file-upload-preview" data-file-upload-target="preview">
|
|
52
|
+
<button type="button"
|
|
53
|
+
class="btn btn-sm btn-outline-secondary mt-2"
|
|
54
|
+
data-action="click->file-upload#clearFile">
|
|
55
|
+
Clear
|
|
56
|
+
</button>
|
|
57
|
+
</div>
|
|
58
|
+
<% end %>
|
|
59
|
+
|
|
60
|
+
<div class="file-upload-status hidden" data-file-upload-target="status">
|
|
61
|
+
<p class="text-text-primary" data-file-upload-target="statusText"></p>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div class="file-upload-error hidden text-alert text-sm mt-2" data-file-upload-target="error"></div>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<script>
|
|
69
|
+
// File upload Stimulus controller inline (temporary, should move to separate file)
|
|
70
|
+
if (!window.fileUploadControllerRegistered) {
|
|
71
|
+
window.fileUploadControllerRegistered = true;
|
|
72
|
+
|
|
73
|
+
if (typeof Stimulus !== 'undefined') {
|
|
74
|
+
Stimulus.register("file-upload", class extends Stimulus.Controller {
|
|
75
|
+
static targets = ["zone", "input", "prompt", "previewContainer", "preview", "status", "statusText", "error"]
|
|
76
|
+
|
|
77
|
+
connect() {
|
|
78
|
+
this.maxSize = <%= max_size * 1024 * 1024 %>; // Convert MB to bytes
|
|
79
|
+
this.setupDragAndDrop();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
setupDragAndDrop() {
|
|
83
|
+
const zone = this.zoneTarget;
|
|
84
|
+
|
|
85
|
+
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
|
86
|
+
zone.addEventListener(eventName, this.preventDefaults.bind(this), false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
['dragenter', 'dragover'].forEach(eventName => {
|
|
90
|
+
zone.addEventListener(eventName, this.highlight.bind(this), false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
['dragleave', 'drop'].forEach(eventName => {
|
|
94
|
+
zone.addEventListener(eventName, this.unhighlight.bind(this), false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
zone.addEventListener('drop', this.handleDrop.bind(this), false);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
preventDefaults(e) {
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
e.stopPropagation();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
highlight(e) {
|
|
106
|
+
this.zoneTarget.classList.add('drag-over');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
unhighlight(e) {
|
|
110
|
+
this.zoneTarget.classList.remove('drag-over');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
handleDrop(e) {
|
|
114
|
+
const dt = e.dataTransfer;
|
|
115
|
+
const files = dt.files;
|
|
116
|
+
this.inputTarget.files = files;
|
|
117
|
+
this.handleFileSelect();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
handleFileSelect() {
|
|
121
|
+
const files = this.inputTarget.files;
|
|
122
|
+
this.hideError();
|
|
123
|
+
|
|
124
|
+
if (files.length === 0) return;
|
|
125
|
+
|
|
126
|
+
// Validate file size
|
|
127
|
+
for (let file of files) {
|
|
128
|
+
if (file.size > this.maxSize) {
|
|
129
|
+
this.showError(`File "${file.name}" exceeds maximum size of <%= max_size %>MB`);
|
|
130
|
+
this.inputTarget.value = '';
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
<% if preview %>
|
|
136
|
+
// Show preview for first image file
|
|
137
|
+
const file = files[0];
|
|
138
|
+
if (file && file.type.startsWith('image/')) {
|
|
139
|
+
const reader = new FileReader();
|
|
140
|
+
reader.onload = (e) => {
|
|
141
|
+
this.previewTarget.src = e.target.result;
|
|
142
|
+
this.promptTarget.classList.add('hidden');
|
|
143
|
+
this.previewContainerTarget.classList.remove('hidden');
|
|
144
|
+
};
|
|
145
|
+
reader.readAsDataURL(file);
|
|
146
|
+
}
|
|
147
|
+
<% else %>
|
|
148
|
+
// Show status
|
|
149
|
+
if (files.length === 1) {
|
|
150
|
+
this.statusTextTarget.textContent = `Selected: ${files[0].name}`;
|
|
151
|
+
} else {
|
|
152
|
+
this.statusTextTarget.textContent = `Selected ${files.length} files`;
|
|
153
|
+
}
|
|
154
|
+
this.promptTarget.classList.add('hidden');
|
|
155
|
+
this.statusTarget.classList.remove('hidden');
|
|
156
|
+
<% end %>
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
clearFile() {
|
|
160
|
+
this.inputTarget.value = '';
|
|
161
|
+
<% if preview %>
|
|
162
|
+
this.promptTarget.classList.remove('hidden');
|
|
163
|
+
this.previewContainerTarget.classList.add('hidden');
|
|
164
|
+
this.previewTarget.src = '';
|
|
165
|
+
<% else %>
|
|
166
|
+
this.promptTarget.classList.remove('hidden');
|
|
167
|
+
this.statusTarget.classList.add('hidden');
|
|
168
|
+
<% end %>
|
|
169
|
+
this.hideError();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
showError(message) {
|
|
173
|
+
this.errorTarget.textContent = message;
|
|
174
|
+
this.errorTarget.classList.remove('hidden');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
hideError() {
|
|
178
|
+
this.errorTarget.classList.add('hidden');
|
|
179
|
+
this.errorTarget.textContent = '';
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
</script>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<%
|
|
2
|
+
# Use configuration from the gem if available, otherwise fall back to defaults
|
|
3
|
+
footer_config = NeonSakura.config
|
|
4
|
+
app_name = footer_config.app_name
|
|
5
|
+
%>
|
|
6
|
+
|
|
7
|
+
<footer class="footer-container" role="contentinfo" aria-label="Site footer">
|
|
8
|
+
<div class="footer-content">
|
|
9
|
+
<p class="footer-text">
|
|
10
|
+
<%= app_name %>, Copyright <%= Time.current.year %>.
|
|
11
|
+
<% if defined?(user_signed_in?) && user_signed_in? %>
|
|
12
|
+
<% if footer_config.respond_to?(:version_module) && footer_config.version_module %>
|
|
13
|
+
<span class="footer-version">Build Version: <%= footer_config.version_module.commit_hash %></span>
|
|
14
|
+
<% end %>
|
|
15
|
+
<% end %>
|
|
16
|
+
<div class="col-md-6 text-md-end">
|
|
17
|
+
<small class="text-muted">
|
|
18
|
+
Built with <span class="text-red-500">♥</span> using Rails
|
|
19
|
+
</small>
|
|
20
|
+
</div>
|
|
21
|
+
</p>
|
|
22
|
+
</div>
|
|
23
|
+
</footer>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<%
|
|
2
|
+
# Use configuration from the gem if available, otherwise fall back to defaults
|
|
3
|
+
header_config = NeonSakura.config
|
|
4
|
+
show_header = header_config.show_header
|
|
5
|
+
app_name = header_config.app_name
|
|
6
|
+
show_version = header_config.show_version
|
|
7
|
+
nav_links = header_config.nav_links
|
|
8
|
+
%>
|
|
9
|
+
|
|
10
|
+
<% if show_header != false %>
|
|
11
|
+
<header class="header-container" role="banner" aria-label="Site header">
|
|
12
|
+
<div class="header-content">
|
|
13
|
+
<div class="header-main">
|
|
14
|
+
<h1 class="header-title">
|
|
15
|
+
<%= app_name %>
|
|
16
|
+
</h1>
|
|
17
|
+
|
|
18
|
+
<% if nav_links && nav_links.any? %>
|
|
19
|
+
<nav class="header-navigation" role="navigation">
|
|
20
|
+
<ul class="nav-list">
|
|
21
|
+
<% nav_links.each do |link| %>
|
|
22
|
+
<li class="nav-item">
|
|
23
|
+
<a href="<%= link[:path] %>" class="nav-link">
|
|
24
|
+
<%= link[:name] %>
|
|
25
|
+
</a>
|
|
26
|
+
</li>
|
|
27
|
+
<% end %>
|
|
28
|
+
</ul>
|
|
29
|
+
</nav>
|
|
30
|
+
<% end %>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<% if show_version %>
|
|
34
|
+
<% if header_config.respond_to?(:version_module) && header_config.version_module %>
|
|
35
|
+
<div class="header-version">
|
|
36
|
+
Version <%= header_config.version_module.commit_hash %>
|
|
37
|
+
</div>
|
|
38
|
+
<% end %>
|
|
39
|
+
<% end %>
|
|
40
|
+
</div>
|
|
41
|
+
</header>
|
|
42
|
+
<% end %>
|