shiplog 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +59 -0
- data/README.md +140 -5
- data/app/controllers/shiplog/admin/area_detections_controller.rb +15 -0
- data/app/controllers/shiplog/admin/area_translations_controller.rb +19 -0
- data/app/controllers/shiplog/admin/areas/merges_controller.rb +19 -0
- data/app/controllers/shiplog/admin/areas/moves_controller.rb +13 -0
- data/app/controllers/shiplog/admin/areas_controller.rb +48 -0
- data/app/controllers/shiplog/admin/entries/images/moves_controller.rb +20 -0
- data/app/controllers/shiplog/admin/entries/images_controller.rb +76 -0
- data/app/controllers/shiplog/admin/entries/translations/retranslations_controller.rb +23 -0
- data/app/controllers/shiplog/admin/entries/translations_controller.rb +35 -0
- data/app/controllers/shiplog/admin/entries_controller.rb +9 -3
- data/app/controllers/shiplog/changelog_entries_controller.rb +26 -0
- data/app/controllers/shiplog/changelog_feeds_controller.rb +19 -0
- data/app/controllers/shiplog/changelogs_controller.rb +21 -5
- data/app/controllers/shiplog/public_controller.rb +55 -0
- data/app/controllers/shiplog/roadmaps_controller.rb +3 -1
- data/app/frontend/shiplog/admin.css +9 -0
- data/app/frontend/shiplog/shiplog.css +47 -2
- data/app/helpers/shiplog/admin/entries_helper.rb +15 -0
- data/app/helpers/shiplog/permalink_helper.rb +113 -0
- data/app/helpers/shiplog/roadmap_helper.rb +9 -0
- data/app/helpers/shiplog/unseen_helper.rb +9 -0
- data/app/jobs/shiplog/detect_areas_job.rb +18 -0
- data/app/jobs/shiplog/rewrite_entry_job.rb +4 -1
- data/app/jobs/shiplog/translate_area_job.rb +25 -0
- data/app/jobs/shiplog/translate_entry_job.rb +25 -0
- data/app/models/shiplog/area.rb +156 -0
- data/app/models/shiplog/area_detector.rb +146 -0
- data/app/models/shiplog/area_translator.rb +77 -0
- data/app/models/shiplog/entry/images.rb +127 -0
- data/app/models/shiplog/entry.rb +204 -5
- data/app/models/shiplog/entry_translation.rb +15 -0
- data/app/models/shiplog/image_fetcher/downloader.rb +98 -0
- data/app/models/shiplog/image_fetcher/sources.rb +41 -0
- data/app/models/shiplog/image_fetcher.rb +72 -0
- data/app/models/shiplog/image_type.rb +111 -0
- data/app/models/shiplog/rewriter/prompt.rb +31 -7
- data/app/models/shiplog/rewriter.rb +41 -23
- data/app/models/shiplog/setting.rb +19 -0
- data/app/models/shiplog/translator/prompt.rb +40 -0
- data/app/models/shiplog/translator.rb +99 -0
- data/app/views/layouts/shiplog/application.html.erb +3 -0
- data/app/views/shiplog/_empty.html.erb +1 -1
- data/app/views/shiplog/_pagination.html.erb +3 -3
- data/app/views/shiplog/admin/_nav.html.erb +2 -1
- data/app/views/shiplog/admin/areas/index.html.erb +78 -0
- data/app/views/shiplog/admin/entries/_entry_row.html.erb +3 -0
- data/app/views/shiplog/admin/entries/_images.html.erb +41 -0
- data/app/views/shiplog/admin/entries/_translations.html.erb +42 -0
- data/app/views/shiplog/admin/entries/edit.html.erb +12 -0
- data/app/views/shiplog/changelog_entries/show.html.erb +25 -0
- data/app/views/shiplog/changelog_feeds/show.atom.builder +24 -0
- data/app/views/shiplog/changelogs/_areas.html.erb +8 -0
- data/app/views/shiplog/changelogs/_entry.html.erb +2 -5
- data/app/views/shiplog/changelogs/_entry_content.html.erb +10 -0
- data/app/views/shiplog/changelogs/_search.html.erb +4 -2
- data/app/views/shiplog/changelogs/show.html.erb +11 -4
- data/app/views/shiplog/roadmaps/_card.html.erb +2 -2
- data/app/views/shiplog/roadmaps/_column.html.erb +3 -3
- data/app/views/shiplog/roadmaps/_shipped.html.erb +1 -1
- data/app/views/shiplog/roadmaps/show.html.erb +2 -2
- data/config/admin_routes.rb +16 -0
- data/config/locales/en.yml +85 -0
- data/config/locales/es.yml +223 -0
- data/config/routes.rb +5 -0
- data/db/migrate/20261027000002_shiplog_0_3_0.rb +105 -0
- data/lib/generators/shiplog/install/install_generator.rb +10 -1
- data/lib/generators/shiplog/install/templates/initializer.rb.tt +45 -1
- data/lib/shiplog/classes.rb +10 -1
- data/lib/shiplog/configuration.rb +92 -3
- data/lib/shiplog/engine.rb +3 -0
- data/lib/shiplog/github/queries.rb +13 -1
- data/lib/shiplog/version.rb +1 -1
- data/lib/shiplog.rb +73 -0
- data/lib/tasks/shiplog.rake +29 -0
- metadata +39 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<%# locals: (areas:, current: nil, query: nil) %>
|
|
2
|
+
<%# the area filter: an "All" link and one link per area (GET ?area=slug, works without JavaScript); the search query is kept %>
|
|
3
|
+
<nav class="<%= shiplog_class(:areas) %>" aria-label="<%= t("shiplog.changelog.areas.label", default: "Filter by area") %>">
|
|
4
|
+
<%= link_to t("shiplog.changelog.areas.all", default: "All"), changelog_path(q: query), class: shiplog_class(:area_link, ("current" unless current)), "aria-current": ("page" unless current) %>
|
|
5
|
+
<% areas.each do |area| %>
|
|
6
|
+
<%= link_to area.display_name, changelog_path(area: area.slug, q: query), class: shiplog_class(:area_link, ("current" if area == current)), "aria-current": ("page" if area == current) %>
|
|
7
|
+
<% end %>
|
|
8
|
+
</nav>
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
<%# locals: (entry:) %>
|
|
2
|
-
<%# one changelog entry
|
|
2
|
+
<%# one changelog entry in the list; the entry itself is the entry_content partial, shared with the permalink page %>
|
|
3
3
|
<li class="<%= token_list(shiplog_class(:entry, entry.kind), (shiplog_class(:entry_feature) if entry.feature?)) %>">
|
|
4
|
-
|
|
5
|
-
<time class="<%= shiplog_class(:entry_date) %>" datetime="<%= entry.released_on.iso8601 %>"><%= l(entry.released_on, format: t("shiplog.date_format", default: "%-d %B %Y")) %></time>
|
|
6
|
-
<h3 class="<%= shiplog_class(:entry_title) %>"><%= entry.title %></h3>
|
|
7
|
-
<div class="<%= shiplog_class(:entry_body) %>"><%= simple_format(h(entry.body)) %></div>
|
|
4
|
+
<%= render "shiplog/changelogs/entry_content", entry:, heading: "h3", linked: true %>
|
|
8
5
|
</li>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<%# locals: (entry:, heading: "h3", linked: false, all_images: !linked) %>
|
|
2
|
+
<%# what an entry shows: category pill ("New" for features, "Improved" for changes), date, title (a permalink link in the list) and body. The area tag (features and changes only) follows the category. The images follow the body: the first one in the list, all of them on the permalink page. %>
|
|
3
|
+
<span class="<%= shiplog_class(:entry_category) %>"><%= t("shiplog.categories.#{entry.kind}", default: (entry.feature? ? "New" : "Improved")) %></span>
|
|
4
|
+
<% if entry.area && Shiplog::Entry::CHANGELOG_KINDS.include?(entry.kind) %>
|
|
5
|
+
<span class="<%= shiplog_class(:entry_area) %>"><%= entry.area.display_name %></span>
|
|
6
|
+
<% end %>
|
|
7
|
+
<time class="<%= shiplog_class(:entry_date) %>" datetime="<%= entry.released_on.iso8601 %>"><%= l(entry.released_on, format: t("shiplog.date_format", default: "%-d %B %Y")) %></time>
|
|
8
|
+
<%= content_tag heading, (linked && shiplog_entry_path(entry) ? link_to(entry.display_title, shiplog_entry_path(entry)) : entry.display_title), class: shiplog_class(:entry_title) %>
|
|
9
|
+
<div class="<%= shiplog_class(:entry_body) %>"><%= simple_format(h(entry.display_body)) %></div>
|
|
10
|
+
<%= shiplog_entry_images(entry, limit: (all_images ? nil : 1)) %>
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
<%# locals: (query: nil, count: nil) %>
|
|
2
|
-
<%# the
|
|
2
|
+
<%# the selected area is read from @area, not a local: a copy of this partial from 0.2.0 declares only query and count %>
|
|
3
|
+
<%# the changelog search form (GET, works without JavaScript); with a query, also the result summary and a link back to the changelog (of the same area, when one is selected) %>
|
|
3
4
|
<% label = t("shiplog.changelog.search.label", default: "Search the changelog") %>
|
|
4
5
|
<form class="<%= shiplog_class(:search) %>" role="search" action="<%= changelog_path %>" method="get">
|
|
5
6
|
<input class="<%= shiplog_class(:search_input) %>" type="search" name="q" value="<%= query %>" aria-label="<%= label %>" placeholder="<%= label %>" maxlength="<%= Shiplog::Entry::SEARCH_MAX_LENGTH %>">
|
|
7
|
+
<% if @area %><input type="hidden" name="area" value="<%= @area.slug %>"><% end %>
|
|
6
8
|
<button class="<%= shiplog_class(:search_button) %>" type="submit"><%= t("shiplog.changelog.search.submit", default: "Search") %></button>
|
|
7
9
|
</form>
|
|
8
10
|
<% if query %>
|
|
@@ -11,6 +13,6 @@
|
|
|
11
13
|
<% if count.to_i.positive? %>
|
|
12
14
|
<%= t("shiplog.changelog.search.summary", count: count, query: query, default: { one: "%{count} result for “%{query}”", other: "%{count} results for “%{query}”" }) %>
|
|
13
15
|
<% end %>
|
|
14
|
-
<%= link_to t("shiplog.changelog.search.clear", default: "Clear search"), changelog_path, class: shiplog_class(:search_clear) %>
|
|
16
|
+
<%= link_to t("shiplog.changelog.search.clear", default: "Clear search"), changelog_path(area: @area&.slug), class: shiplog_class(:search_clear) %>
|
|
15
17
|
</p>
|
|
16
18
|
<% end %>
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
<% content_for :title, t("shiplog.changelog.title") %>
|
|
1
|
+
<% content_for :title, t("shiplog.changelog.title", default: "Changelog") %>
|
|
2
|
+
<% content_for :shiplog_feed, auto_discovery_link_tag(:atom, shiplog_absolute_url(:changelog_feed), title: t("shiplog.feed.link_title", default: "Changelog (Atom)")) %>
|
|
2
3
|
<%= render layout: "shiplog/page", locals: { name: "changelog" } do %>
|
|
3
|
-
<%= render "shiplog/page_header", title: t("shiplog.changelog.title"), lede: t("shiplog.changelog.lede") %>
|
|
4
|
+
<%= render "shiplog/page_header", title: t("shiplog.changelog.title", default: "Changelog"), lede: t("shiplog.changelog.lede", default: "New features and improvements, as they ship.") %>
|
|
4
5
|
<% if @query %>
|
|
5
6
|
<% content_for :shiplog_robots, "noindex" %>
|
|
6
7
|
<% end %>
|
|
7
8
|
<% if Shiplog.config.changelog_search %>
|
|
8
9
|
<%= render "shiplog/changelogs/search", query: @query, count: @result_count %>
|
|
9
10
|
<% end %>
|
|
11
|
+
<% if @areas.any? %>
|
|
12
|
+
<%= render "shiplog/changelogs/areas", areas: @areas, current: @area, query: @query %>
|
|
13
|
+
<% end %>
|
|
10
14
|
<% if @months.empty? && @query %>
|
|
11
15
|
<p class="<%= shiplog_class(:search_empty) %>"><%= t("shiplog.changelog.search.empty", query: @query, default: "No entries match “%{query}”.") %></p>
|
|
12
16
|
<% elsif @months.empty? %>
|
|
@@ -19,6 +23,9 @@
|
|
|
19
23
|
</div>
|
|
20
24
|
<% end %>
|
|
21
25
|
<%= render "shiplog/pagination",
|
|
22
|
-
newer_path: (changelog_path(page: (@page - 1 if @page > 2), q: @query) if @page > 1),
|
|
23
|
-
older_path: (changelog_path(page: @page + 1, q: @query) if @older) %>
|
|
26
|
+
newer_path: (changelog_path(area: @area&.slug, page: (@page - 1 if @page > 2), q: @query) if @page > 1),
|
|
27
|
+
older_path: (changelog_path(area: @area&.slug, page: @page + 1, q: @query) if @older) %>
|
|
28
|
+
<p class="<%= shiplog_class(:footnote) %>">
|
|
29
|
+
<%= link_to t("shiplog.feed.subscribe", default: "Subscribe (Atom)"), changelog_feed_path, class: shiplog_class(:footnote_link, :feed) %>
|
|
30
|
+
</p>
|
|
24
31
|
<% end %>
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
parts = issue.children.size
|
|
7
7
|
%>
|
|
8
8
|
<li class="<%= shiplog_class(:card) %>">
|
|
9
|
-
<h3 class="<%= shiplog_class(:card_title) %>"><%= entry.
|
|
10
|
-
<div class="<%= shiplog_class(:card_body) %>"><%= simple_format(h(entry.
|
|
9
|
+
<h3 class="<%= shiplog_class(:card_title) %>"><%= entry.display_title %></h3>
|
|
10
|
+
<div class="<%= shiplog_class(:card_body) %>"><%= simple_format(h(entry.display_body)) %></div>
|
|
11
11
|
<% if tag_label || parts > 1 %>
|
|
12
12
|
<p class="<%= shiplog_class(:card_meta) %>">
|
|
13
13
|
<% if tag_label %><span class="<%= shiplog_class(:card_tag) %>"><%= tag_label %></span><% end %>
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
<% heading_id = "shiplog-column-#{column.dasherize}" %>
|
|
4
4
|
<section class="<%= shiplog_class(:column, column) %>" aria-labelledby="<%= heading_id %>">
|
|
5
5
|
<header class="<%= shiplog_class(:column_header) %>">
|
|
6
|
-
<h2 class="<%= shiplog_class(:column_title) %>" id="<%= heading_id %>"><span class="<%= shiplog_class(:column_marker, column) %>" aria-hidden="true"></span><%= t("shiplog.columns.#{column}") %></h2>
|
|
6
|
+
<h2 class="<%= shiplog_class(:column_title) %>" id="<%= heading_id %>"><span class="<%= shiplog_class(:column_marker, column) %>" aria-hidden="true"></span><%= t("shiplog.columns.#{column}", default: shiplog_column_defaults.fetch(column.to_sym, {})[:title] || column.humanize) %></h2>
|
|
7
7
|
<span class="<%= shiplog_class(:column_count) %>"><%= entries.size %></span>
|
|
8
|
-
<p class="<%= shiplog_class(:column_description) %>"><%= t("shiplog.columns.#{column}_description") %></p>
|
|
8
|
+
<p class="<%= shiplog_class(:column_description) %>"><%= t("shiplog.columns.#{column}_description", default: shiplog_column_defaults.fetch(column.to_sym, {})[:description].to_s) %></p>
|
|
9
9
|
</header>
|
|
10
10
|
<% if entries.empty? %>
|
|
11
|
-
<p class="<%= shiplog_class(:column_empty) %>"><%= t("shiplog.empty") %></p>
|
|
11
|
+
<p class="<%= shiplog_class(:column_empty) %>"><%= t("shiplog.empty", default: "Nothing here yet") %></p>
|
|
12
12
|
<% else %>
|
|
13
13
|
<ol class="<%= shiplog_class(:cards) %>">
|
|
14
14
|
<%= render partial: "shiplog/roadmaps/card", collection: entries, as: :entry %>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<% entries.each do |entry| %>
|
|
9
9
|
<li class="<%= shiplog_class(:shipped_item) %>">
|
|
10
10
|
<time class="<%= shiplog_class(:shipped_date) %>" datetime="<%= entry.released_on.iso8601 %>"><%= l(entry.released_on, format: t("shiplog.roadmap.shipped_date_format", default: "%-d %B")) %></time>
|
|
11
|
-
<%= link_to entry.
|
|
11
|
+
<%= link_to entry.display_title, changelog_path(anchor: "shiplog-month-#{entry.released_on.strftime("%Y-%m")}"), class: shiplog_class(:shipped_link) %>
|
|
12
12
|
</li>
|
|
13
13
|
<% end %>
|
|
14
14
|
</ul>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
<% content_for :title, t("shiplog.roadmap.title") %>
|
|
1
|
+
<% content_for :title, t("shiplog.roadmap.title", default: "Roadmap") %>
|
|
2
2
|
<%= render layout: "shiplog/page", locals: { name: "roadmap" } do %>
|
|
3
|
-
<%= render "shiplog/page_header", title: t("shiplog.roadmap.title"), lede: t("shiplog.roadmap.lede") %>
|
|
3
|
+
<%= render "shiplog/page_header", title: t("shiplog.roadmap.title", default: "Roadmap"), lede: t("shiplog.roadmap.lede", default: "What we're working on and what comes next.") %>
|
|
4
4
|
<div class="<%= shiplog_class(:columns) %>">
|
|
5
5
|
<% @columns.each do |column, items| %>
|
|
6
6
|
<%= render "shiplog/roadmaps/column", column:, entries: items.map(&:entry) %>
|
data/config/admin_routes.rb
CHANGED
|
@@ -5,6 +5,22 @@ Shiplog::Admin::Engine.routes.draw do
|
|
|
5
5
|
resource :approval, only: :create
|
|
6
6
|
resource :discard, only: %i[create destroy]
|
|
7
7
|
resource :rewrite, only: :create
|
|
8
|
+
# The locale segment is :translation_locale, not :locale: a host's around_action that switches the locale
|
|
9
|
+
# from params[:locale] must not read it (a configured translation locale may not be an available one).
|
|
10
|
+
locale = { translation_locale: /[A-Za-z][A-Za-z0-9_-]*/ }
|
|
11
|
+
patch "translations/:translation_locale", to: "translations#update", as: :translation, constraints: locale
|
|
12
|
+
post "translations/:translation_locale/retranslation", to: "translations/retranslations#create", as: :translation_retranslation, constraints: locale
|
|
13
|
+
resources :images, only: %i[create update destroy] do
|
|
14
|
+
resource :move, only: :create, module: :images
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
resource :area_detection, only: :create, path: "areas/detection"
|
|
19
|
+
resource :area_translation, only: :create, path: "areas/translation"
|
|
20
|
+
resources :areas, only: %i[index update destroy] do
|
|
21
|
+
scope module: :areas do
|
|
22
|
+
resource :merge, only: :create
|
|
23
|
+
resource :move, only: :create
|
|
8
24
|
end
|
|
9
25
|
end
|
|
10
26
|
resource :board, only: %i[show update]
|
data/config/locales/en.yml
CHANGED
|
@@ -46,12 +46,61 @@ en:
|
|
|
46
46
|
other: "%{count} results for “%{query}”"
|
|
47
47
|
clear: Clear search
|
|
48
48
|
empty: "No entries match “%{query}”."
|
|
49
|
+
areas:
|
|
50
|
+
label: Filter by area
|
|
51
|
+
all: All
|
|
52
|
+
permalink:
|
|
53
|
+
title: "%{title} · %{changelog}"
|
|
54
|
+
back: "← Back to the changelog"
|
|
55
|
+
nav_label: More changelog entries
|
|
56
|
+
newer: "← Newer: %{title}"
|
|
57
|
+
older: "Older: %{title} →"
|
|
58
|
+
feed:
|
|
59
|
+
title_with_product: "%{product} changelog"
|
|
60
|
+
link_title: Changelog (Atom)
|
|
61
|
+
subscribe: Subscribe (Atom)
|
|
49
62
|
admin:
|
|
50
63
|
title: Shiplog admin
|
|
51
64
|
nav:
|
|
52
65
|
drafts: Drafts
|
|
53
66
|
board: Roadmap board
|
|
54
67
|
sync: Sync
|
|
68
|
+
areas: Areas
|
|
69
|
+
areas:
|
|
70
|
+
title: Product areas
|
|
71
|
+
help: The AI groups entries into areas (at most %{max}) while it rewrites them. Visitors can filter the changelog by area once two areas have published entries.
|
|
72
|
+
detect: Detect areas
|
|
73
|
+
recheck: Re-check unplaced entries
|
|
74
|
+
recheck_help: Ask the AI again about entries it left without an area, e.g. after renaming, merging or adding areas.
|
|
75
|
+
no_rewriter: Detection needs config.rewriter, which is not set.
|
|
76
|
+
last_detection: "Last detection: %{time}, assigned %{count}"
|
|
77
|
+
last_error: "Last detection failed (%{time}): %{error}"
|
|
78
|
+
detection_queued: Detection queued. Refresh in a moment.
|
|
79
|
+
translate_names: Translate names
|
|
80
|
+
translate_names_help: Ask the AI for the names that are missing in some language. Names that are already there, including ones you edited, are kept.
|
|
81
|
+
no_locales: Translating names needs config.locales, which is empty.
|
|
82
|
+
no_rewriter_translate: Translating names needs config.rewriter, which is not set.
|
|
83
|
+
translation_queued:
|
|
84
|
+
one: Translation queued for %{count} area. Refresh in a moment.
|
|
85
|
+
other: Translation queued for %{count} areas. Refresh in a moment.
|
|
86
|
+
empty: No areas yet. Detect areas to propose some from your entries.
|
|
87
|
+
columns:
|
|
88
|
+
name: Name
|
|
89
|
+
entries: Entries
|
|
90
|
+
order: Order
|
|
91
|
+
merge: Merge into
|
|
92
|
+
name_label: Name
|
|
93
|
+
rename: Rename
|
|
94
|
+
renamed: Area renamed.
|
|
95
|
+
move_up: Move up
|
|
96
|
+
move_down: Move down
|
|
97
|
+
merge: Merge
|
|
98
|
+
merge_into: Merge into
|
|
99
|
+
merged: "%{from} merged into %{into}."
|
|
100
|
+
merge_target: Choose another area to merge into.
|
|
101
|
+
merge_help: Merging moves the entries to the other area and deletes this one. Deleting an area leaves its entries without one.
|
|
102
|
+
delete: Delete
|
|
103
|
+
deleted: Area deleted. Its entries have no area now.
|
|
55
104
|
entries:
|
|
56
105
|
title: Entries
|
|
57
106
|
filters: Filter entries
|
|
@@ -73,6 +122,8 @@ en:
|
|
|
73
122
|
restore: Restore
|
|
74
123
|
save: Save
|
|
75
124
|
regenerate: Regenerate
|
|
125
|
+
no_area: None
|
|
126
|
+
area_gone: no longer exists
|
|
76
127
|
regenerate_warning: Regenerating unpublishes this entry until re-approved.
|
|
77
128
|
source_changed: GitHub text changed since this draft
|
|
78
129
|
rewrite_error: Rewrite failed
|
|
@@ -80,6 +131,39 @@ en:
|
|
|
80
131
|
public_text: Public text
|
|
81
132
|
source: GitHub source
|
|
82
133
|
back: Back to drafts
|
|
134
|
+
images:
|
|
135
|
+
title: Screenshots
|
|
136
|
+
help: Shown under the text on the changelog (the first one in the list, all of them on the entry's page). Taken from the GitHub issue or pull request when the draft is written; you can also upload your own.
|
|
137
|
+
empty: No screenshots yet.
|
|
138
|
+
alt: Alt text
|
|
139
|
+
alt_placeholder: Describe the image (defaults to the title)
|
|
140
|
+
save_alt: Save alt text
|
|
141
|
+
remove: Remove
|
|
142
|
+
move_up: Up
|
|
143
|
+
move_down: Down
|
|
144
|
+
upload: Upload images
|
|
145
|
+
upload_label: Images (PNG, JPEG, GIF or WebP, up to 5 MB each)
|
|
146
|
+
none_selected: Choose at least one image.
|
|
147
|
+
rejected: "Not added, only PNG, JPEG, GIF and WebP images up to 5 MB and 10,000 pixels a side are accepted: %{names}"
|
|
148
|
+
added:
|
|
149
|
+
one: Image added.
|
|
150
|
+
other: "%{count} images added."
|
|
151
|
+
alt_saved: Alt text saved.
|
|
152
|
+
removed: Image removed.
|
|
153
|
+
moved: Image moved.
|
|
154
|
+
translations:
|
|
155
|
+
title: Translations
|
|
156
|
+
help: "Approving an entry translates it automatically, and the translation is published with the English text. A translation you edit is kept until the English text changes; then it is translated again. A stale or missing translation is not shown: visitors see the English text."
|
|
157
|
+
not_approved: Translations are written when the entry is approved.
|
|
158
|
+
missing: Missing
|
|
159
|
+
stale: Stale
|
|
160
|
+
current: Up to date
|
|
161
|
+
edited: Edited by an admin
|
|
162
|
+
retranslate: Retranslate
|
|
163
|
+
saved: Translation saved.
|
|
164
|
+
queued: Translation queued. Refresh in a moment.
|
|
165
|
+
only_approved: Only approved entries are translated.
|
|
166
|
+
no_rewriter: Translating needs config.rewriter, which is not set.
|
|
83
167
|
category_options:
|
|
84
168
|
feature: New feature
|
|
85
169
|
change: Improvement
|
|
@@ -101,6 +185,7 @@ en:
|
|
|
101
185
|
body: Text
|
|
102
186
|
released_on: Released on
|
|
103
187
|
category: Category
|
|
188
|
+
area: Area
|
|
104
189
|
reason: Reason
|
|
105
190
|
board:
|
|
106
191
|
title: Roadmap board
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# Textos de Shiplog en español. Los hosts pueden sobrescribir cualquier clave en sus propios archivos de idioma.
|
|
2
|
+
es:
|
|
3
|
+
activerecord:
|
|
4
|
+
attributes:
|
|
5
|
+
shiplog/entry:
|
|
6
|
+
changelog_kind: Categoría
|
|
7
|
+
# Los nombres de los meses que usa el formato de fecha de Shiplog (rails-i18n los trae también; son los mismos).
|
|
8
|
+
date:
|
|
9
|
+
month_names: [~, enero, febrero, marzo, abril, mayo, junio, julio, agosto, septiembre, octubre, noviembre, diciembre]
|
|
10
|
+
shiplog:
|
|
11
|
+
columns:
|
|
12
|
+
in_progress: En desarrollo
|
|
13
|
+
coming_soon: Próximamente
|
|
14
|
+
later: Más adelante
|
|
15
|
+
in_progress_description: Lo que estamos construyendo ahora
|
|
16
|
+
coming_soon_description: Lo siguiente en la lista
|
|
17
|
+
later_description: En nuestro radar
|
|
18
|
+
categories:
|
|
19
|
+
feature: Nuevo
|
|
20
|
+
change: Mejorado
|
|
21
|
+
empty: Todavía no hay nada
|
|
22
|
+
date_format: "%-d de %B de %Y"
|
|
23
|
+
roadmap:
|
|
24
|
+
title: Hoja de ruta
|
|
25
|
+
lede: En qué estamos trabajando y qué viene después.
|
|
26
|
+
parts:
|
|
27
|
+
one: Incluye %{count} parte
|
|
28
|
+
other: Incluye %{count} partes
|
|
29
|
+
shipped_heading: Lanzado recientemente
|
|
30
|
+
shipped_date_format: "%-d de %B"
|
|
31
|
+
full_changelog: Ver el registro de cambios completo
|
|
32
|
+
footnote: ""
|
|
33
|
+
suggest: Sugerir una función
|
|
34
|
+
switcher:
|
|
35
|
+
label: Actualizaciones del producto
|
|
36
|
+
roadmap: Hoja de ruta
|
|
37
|
+
changelog: Registro de cambios
|
|
38
|
+
changelog:
|
|
39
|
+
title: Registro de cambios
|
|
40
|
+
lede: Nuevas funciones y mejoras, a medida que se lanzan.
|
|
41
|
+
pagination:
|
|
42
|
+
label: Paginación
|
|
43
|
+
older: Anteriores
|
|
44
|
+
newer: Más recientes
|
|
45
|
+
search:
|
|
46
|
+
label: Buscar en el registro de cambios
|
|
47
|
+
submit: Buscar
|
|
48
|
+
summary:
|
|
49
|
+
one: "%{count} resultado para “%{query}”"
|
|
50
|
+
other: "%{count} resultados para “%{query}”"
|
|
51
|
+
clear: Borrar búsqueda
|
|
52
|
+
empty: "Ninguna entrada coincide con “%{query}”."
|
|
53
|
+
areas:
|
|
54
|
+
label: Filtrar por área
|
|
55
|
+
all: Todas
|
|
56
|
+
permalink:
|
|
57
|
+
title: "%{title} · %{changelog}"
|
|
58
|
+
back: "← Volver al registro de cambios"
|
|
59
|
+
nav_label: Más cambios
|
|
60
|
+
newer: "← Más reciente: %{title}"
|
|
61
|
+
older: "Anterior: %{title} →"
|
|
62
|
+
feed:
|
|
63
|
+
title_with_product: "Registro de cambios de %{product}"
|
|
64
|
+
link_title: Registro de cambios (Atom)
|
|
65
|
+
subscribe: Suscribirse (Atom)
|
|
66
|
+
admin:
|
|
67
|
+
title: Administración de Shiplog
|
|
68
|
+
nav:
|
|
69
|
+
drafts: Borradores
|
|
70
|
+
board: Tablero de la hoja de ruta
|
|
71
|
+
sync: Sincronización
|
|
72
|
+
areas: Áreas
|
|
73
|
+
areas:
|
|
74
|
+
title: Áreas del producto
|
|
75
|
+
help: La IA agrupa las entradas en áreas (como máximo %{max}) mientras las reescribe. Los visitantes pueden filtrar el registro de cambios por área cuando hay dos áreas con entradas publicadas.
|
|
76
|
+
detect: Detectar áreas
|
|
77
|
+
recheck: Revisar de nuevo las entradas sin área
|
|
78
|
+
recheck_help: Vuelve a preguntar a la IA por las entradas que dejó sin área, por ejemplo después de renombrar, fusionar o añadir áreas.
|
|
79
|
+
no_rewriter: La detección necesita config.rewriter, que no está configurado.
|
|
80
|
+
last_detection: "Última detección: %{time}, %{count} asignadas"
|
|
81
|
+
last_error: "La última detección falló (%{time}): %{error}"
|
|
82
|
+
detection_queued: Detección en cola. Actualiza en un momento.
|
|
83
|
+
translate_names: Traducir nombres
|
|
84
|
+
translate_names_help: Pide a la IA los nombres que faltan en algún idioma. Los nombres que ya existen, incluidos los que editaste, se conservan.
|
|
85
|
+
no_locales: Traducir los nombres necesita config.locales, que está vacío.
|
|
86
|
+
no_rewriter_translate: Traducir los nombres necesita config.rewriter, que no está configurado.
|
|
87
|
+
translation_queued:
|
|
88
|
+
one: Traducción en cola para %{count} área. Actualiza en un momento.
|
|
89
|
+
other: Traducción en cola para %{count} áreas. Actualiza en un momento.
|
|
90
|
+
empty: Todavía no hay áreas. Detecta áreas para que se propongan a partir de tus entradas.
|
|
91
|
+
columns:
|
|
92
|
+
name: Nombre
|
|
93
|
+
entries: Entradas
|
|
94
|
+
order: Orden
|
|
95
|
+
merge: Fusionar con
|
|
96
|
+
name_label: Nombre
|
|
97
|
+
rename: Renombrar
|
|
98
|
+
renamed: Área renombrada.
|
|
99
|
+
move_up: Subir
|
|
100
|
+
move_down: Bajar
|
|
101
|
+
merge: Fusionar
|
|
102
|
+
merge_into: Fusionar con
|
|
103
|
+
merged: "%{from} se fusionó con %{into}."
|
|
104
|
+
merge_target: Elige otra área con la que fusionar.
|
|
105
|
+
merge_help: Al fusionar, las entradas pasan a la otra área y esta se elimina. Si eliminas un área, sus entradas se quedan sin área.
|
|
106
|
+
delete: Eliminar
|
|
107
|
+
deleted: Área eliminada. Sus entradas ya no tienen área.
|
|
108
|
+
entries:
|
|
109
|
+
title: Entradas
|
|
110
|
+
filters: Filtrar entradas
|
|
111
|
+
all_kinds: Todos los tipos
|
|
112
|
+
empty: Ninguna entrada coincide.
|
|
113
|
+
pagination: Paginación
|
|
114
|
+
older: Anteriores
|
|
115
|
+
newer: Más recientes
|
|
116
|
+
saved: Entrada guardada.
|
|
117
|
+
approved: Entrada aprobada.
|
|
118
|
+
discarded: Entrada descartada.
|
|
119
|
+
restored: Entrada devuelta a borrador.
|
|
120
|
+
invalid_transition: Esa acción no está disponible para esta entrada en su estado actual.
|
|
121
|
+
rewrite_queued: Reescritura en cola. Actualiza en un momento.
|
|
122
|
+
edit: Editar
|
|
123
|
+
edit_title: "Editar %{kind}"
|
|
124
|
+
approve: Aprobar
|
|
125
|
+
discard: Descartar
|
|
126
|
+
restore: Restaurar
|
|
127
|
+
save: Guardar
|
|
128
|
+
regenerate: Regenerar
|
|
129
|
+
no_area: Ninguna
|
|
130
|
+
area_gone: ya no existe
|
|
131
|
+
regenerate_warning: Al regenerar, esta entrada deja de estar publicada hasta que se apruebe de nuevo.
|
|
132
|
+
source_changed: El texto de GitHub cambió desde este borrador
|
|
133
|
+
rewrite_error: Falló la reescritura
|
|
134
|
+
not_rewritten: Todavía sin reescribir
|
|
135
|
+
public_text: Texto público
|
|
136
|
+
source: Origen en GitHub
|
|
137
|
+
back: Volver a los borradores
|
|
138
|
+
images:
|
|
139
|
+
title: Capturas de pantalla
|
|
140
|
+
help: Se muestran bajo el texto en el registro de cambios (la primera en la lista, todas en la página de la entrada). Se toman de la incidencia o del pull request de GitHub al escribir el borrador; también puedes subir las tuyas.
|
|
141
|
+
empty: Todavía no hay capturas.
|
|
142
|
+
alt: Texto alternativo
|
|
143
|
+
alt_placeholder: Describe la imagen (por defecto, el título)
|
|
144
|
+
save_alt: Guardar texto alternativo
|
|
145
|
+
remove: Quitar
|
|
146
|
+
move_up: Subir
|
|
147
|
+
move_down: Bajar
|
|
148
|
+
upload: Subir imágenes
|
|
149
|
+
upload_label: Imágenes (PNG, JPEG, GIF o WebP, hasta 5 MB cada una)
|
|
150
|
+
none_selected: Elige al menos una imagen.
|
|
151
|
+
rejected: "No se añadieron; solo se aceptan imágenes PNG, JPEG, GIF y WebP de hasta 5 MB y 10.000 píxeles por lado: %{names}"
|
|
152
|
+
added:
|
|
153
|
+
one: Imagen añadida.
|
|
154
|
+
other: "%{count} imágenes añadidas."
|
|
155
|
+
alt_saved: Texto alternativo guardado.
|
|
156
|
+
removed: Imagen quitada.
|
|
157
|
+
moved: Imagen movida.
|
|
158
|
+
translations:
|
|
159
|
+
title: Traducciones
|
|
160
|
+
help: "Al aprobar una entrada, esta se traduce automáticamente, y la traducción se publica junto con el texto en inglés. Una traducción que edites se conserva hasta que cambie el texto en inglés; entonces se vuelve a traducir. Una traducción desactualizada o ausente no se muestra: los visitantes ven el texto en inglés."
|
|
161
|
+
not_approved: Las traducciones se escriben cuando se aprueba la entrada.
|
|
162
|
+
missing: Falta
|
|
163
|
+
stale: Desactualizada
|
|
164
|
+
current: Al día
|
|
165
|
+
edited: Editada por un administrador
|
|
166
|
+
retranslate: Traducir de nuevo
|
|
167
|
+
saved: Traducción guardada.
|
|
168
|
+
queued: Traducción en cola. Actualiza en un momento.
|
|
169
|
+
only_approved: Solo se traducen las entradas aprobadas.
|
|
170
|
+
no_rewriter: Para traducir hace falta config.rewriter, que no está configurado.
|
|
171
|
+
category_options:
|
|
172
|
+
feature: Nueva función
|
|
173
|
+
change: Mejora
|
|
174
|
+
statuses:
|
|
175
|
+
draft: Borradores
|
|
176
|
+
approved: Aprobadas
|
|
177
|
+
discarded: Descartadas
|
|
178
|
+
kinds:
|
|
179
|
+
roadmap: Hoja de ruta
|
|
180
|
+
feature: Función
|
|
181
|
+
change: Cambio
|
|
182
|
+
columns:
|
|
183
|
+
entry: Entrada
|
|
184
|
+
kind: Tipo
|
|
185
|
+
source: Origen
|
|
186
|
+
actions: Acciones
|
|
187
|
+
fields:
|
|
188
|
+
title: Título
|
|
189
|
+
body: Texto
|
|
190
|
+
released_on: Fecha de lanzamiento
|
|
191
|
+
category: Categoría
|
|
192
|
+
area: Área
|
|
193
|
+
reason: Motivo
|
|
194
|
+
board:
|
|
195
|
+
title: Tablero de la hoja de ruta
|
|
196
|
+
help: Arrastra las tarjetas entre columnas para fijarlas y ordenarlas. Sin JavaScript, usa el botón Mover de cada tarjeta.
|
|
197
|
+
updated: Tablero actualizado.
|
|
198
|
+
not_approved: Sin aprobar
|
|
199
|
+
pinned: Fijada
|
|
200
|
+
move_to: Mover a
|
|
201
|
+
move: Mover
|
|
202
|
+
unpin: Quitar de fijadas
|
|
203
|
+
sync:
|
|
204
|
+
title: Estado de la sincronización
|
|
205
|
+
queued: Sincronización en cola.
|
|
206
|
+
webhook_secret_missing: El secreto del webhook no está configurado, así que se rechazan los webhooks de GitHub.
|
|
207
|
+
github_token_missing: El token de GitHub no está configurado, así que no se puede sincronizar.
|
|
208
|
+
sync_now: Sincronizar ahora
|
|
209
|
+
sync_and_scan: Sincronizar y buscar cambios
|
|
210
|
+
repositories: Repositorios
|
|
211
|
+
no_repositories: No hay repositorios configurados.
|
|
212
|
+
repository: Repositorio
|
|
213
|
+
synced_at: Última sincronización
|
|
214
|
+
changes_scanned_at: Cambios revisados
|
|
215
|
+
last_error: Último error
|
|
216
|
+
never: Nunca
|
|
217
|
+
deliveries: Últimas entregas de webhook
|
|
218
|
+
no_deliveries: Todavía no hay entregas.
|
|
219
|
+
event: Evento
|
|
220
|
+
action: Acción
|
|
221
|
+
received_at: Recibida
|
|
222
|
+
processed_at: Procesada
|
|
223
|
+
error: Error
|
data/config/routes.rb
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
Shiplog::Engine.routes.draw do
|
|
2
2
|
get Shiplog.config.roadmap_path, to: "roadmaps#show", as: :roadmap
|
|
3
|
+
# The feed and the permalinks come before the plain changelog route, whose optional (.:format) would
|
|
4
|
+
# otherwise take /changelog.atom. A slug has no dot, and the permalink takes no format.
|
|
5
|
+
get "#{Shiplog.config.changelog_path}.atom", to: "changelog_feeds#show", as: :changelog_feed, defaults: { format: "atom" }
|
|
6
|
+
get "#{Shiplog.config.changelog_path}/:slug", to: "changelog_entries#show", as: :changelog_entry,
|
|
7
|
+
format: false, constraints: { slug: /[a-z0-9-]+/ }
|
|
3
8
|
get Shiplog.config.changelog_path, to: "changelogs#show", as: :changelog
|
|
4
9
|
post "shiplog/github/webhook", to: "webhooks#create", as: :github_webhook
|
|
5
10
|
get "shiplog/assets/:version/:name", to: "frontends#show", as: :frontend, constraints: { version: /[^\/]+/, name: /[a-z_]+\.(css|js)/ }
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Everything 0.3.0 adds to the schema, one section per feature. Additive and reversible.
|
|
2
|
+
class Shiplog030 < ActiveRecord::Migration[8.1]
|
|
3
|
+
# A migration must not depend on model code that changes later: this is a bare table-backed class.
|
|
4
|
+
class MigrationEntry < ActiveRecord::Base
|
|
5
|
+
self.table_name = "shiplog_entries"
|
|
6
|
+
self.inheritance_column = nil
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
SLUG_MAX_LENGTH = 80
|
|
10
|
+
|
|
11
|
+
def change
|
|
12
|
+
# --- Entry permalinks (/changelog/:slug) ---
|
|
13
|
+
# Set when an entry is first approved, never changed afterwards, so links never break. Nil for roadmap
|
|
14
|
+
# entries and drafts. Entries approved before 0.3.0 get theirs here (oldest first, so the oldest keeps the
|
|
15
|
+
# plain slug on duplicate titles): the public pages never write, so nothing else fills them in.
|
|
16
|
+
add_column :shiplog_entries, :slug, :string
|
|
17
|
+
add_index :shiplog_entries, :slug, unique: true, where: "slug IS NOT NULL"
|
|
18
|
+
reversible do |direction|
|
|
19
|
+
direction.up do
|
|
20
|
+
MigrationEntry.reset_column_information
|
|
21
|
+
backfill_slugs
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# --- "What's new" unseen count ---
|
|
26
|
+
# `Shiplog.unseen_count` counts approved feature and change entries with `approved_at > since`:
|
|
27
|
+
# an equality column first, then the range column.
|
|
28
|
+
add_index :shiplog_entries, [ :status, :approved_at ]
|
|
29
|
+
|
|
30
|
+
# --- Product areas (detected by AI) ---
|
|
31
|
+
# `entries.area_id` is nullable; deleting an area nullifies it (merging moves the entries first).
|
|
32
|
+
# `area_set_by_admin` marks an area chosen by hand, which detection never overwrites.
|
|
33
|
+
create_table :shiplog_areas do |t|
|
|
34
|
+
t.string :name, null: false
|
|
35
|
+
t.string :slug, null: false
|
|
36
|
+
t.integer :position, null: false, default: 0
|
|
37
|
+
# The name in each of config.locales, keyed by locale string ({ "es" => "Listas de embalaje" }); empty
|
|
38
|
+
# until translated. Slugs stay English, so ?area= URLs are shared across locales.
|
|
39
|
+
t.jsonb :name_translations, null: false, default: {}
|
|
40
|
+
t.timestamps
|
|
41
|
+
end
|
|
42
|
+
add_index :shiplog_areas, :slug, unique: true
|
|
43
|
+
add_reference :shiplog_entries, :area, null: true, index: true, foreign_key: { to_table: :shiplog_areas, on_delete: :nullify }
|
|
44
|
+
add_column :shiplog_entries, :area_set_by_admin, :boolean, null: false, default: false
|
|
45
|
+
# When detection last asked the AI about the entry (placed or not): unplaced entries are not asked again
|
|
46
|
+
# until an admin asks for a re-check.
|
|
47
|
+
add_column :shiplog_entries, :area_detected_at, :datetime
|
|
48
|
+
# A small key-value store for state that belongs to no record, e.g. the result of the last area detection
|
|
49
|
+
# (shown on the admin's Areas page).
|
|
50
|
+
create_table :shiplog_settings do |t|
|
|
51
|
+
t.string :key, null: false
|
|
52
|
+
t.jsonb :value, null: false, default: {}
|
|
53
|
+
t.timestamps
|
|
54
|
+
end
|
|
55
|
+
add_index :shiplog_settings, :key, unique: true
|
|
56
|
+
|
|
57
|
+
# --- Screenshots (needs Active Storage in the host; the columns are harmless without it) ---
|
|
58
|
+
# `image_order` is the ordered list of the entry's image blob ids, `image_alts` the alt text per blob id
|
|
59
|
+
# ({ "12" => "The dark theme" }); a missing alt text falls back to the entry's title.
|
|
60
|
+
add_column :shiplog_entries, :image_order, :jsonb, null: false, default: []
|
|
61
|
+
add_column :shiplog_entries, :image_alts, :jsonb, null: false, default: {}
|
|
62
|
+
|
|
63
|
+
# --- Translations of entries (config.locales) ---
|
|
64
|
+
# One row per entry and locale, published with the English text (no second approval). `source_digest` is the
|
|
65
|
+
# digest of the English title and body it was made from: a row whose digest differs from the entry's current
|
|
66
|
+
# text is stale and never shown. `edited_by_admin` keeps an admin's edit until the English text changes.
|
|
67
|
+
create_table :shiplog_entry_translations do |t|
|
|
68
|
+
t.references :entry, null: false, index: false, foreign_key: { to_table: :shiplog_entries, on_delete: :cascade }
|
|
69
|
+
t.string :locale, null: false
|
|
70
|
+
t.string :title, null: false
|
|
71
|
+
t.text :body, null: false, default: ""
|
|
72
|
+
t.string :source_digest, null: false
|
|
73
|
+
t.boolean :edited_by_admin, null: false, default: false
|
|
74
|
+
t.timestamps
|
|
75
|
+
end
|
|
76
|
+
add_index :shiplog_entry_translations, [ :entry_id, :locale ], unique: true
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Gives every approved feature and change with a release date, and no slug, one: the title as [a-z0-9-] only
|
|
80
|
+
# (cut at 80 characters), made unique with -2, -3, ...; entry-<id> when nothing is left. The same rules as
|
|
81
|
+
# Shiplog::Entry#generate_slug at the time of 0.3.0.
|
|
82
|
+
def backfill_slugs
|
|
83
|
+
taken = MigrationEntry.where.not(slug: nil).pluck(:slug).to_set
|
|
84
|
+
MigrationEntry.where(status: "approved", kind: %w[feature change], slug: nil).where.not(released_on: nil)
|
|
85
|
+
.order(:released_on, :approved_at, :id).to_a.each do |entry|
|
|
86
|
+
slug = slug_for(entry, taken)
|
|
87
|
+
taken << slug
|
|
88
|
+
MigrationEntry.where(id: entry.id, slug: nil).update_all(slug:)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def slug_for(entry, taken)
|
|
95
|
+
base = entry.title.to_s.parameterize.tr("_", "-").squeeze("-").first(SLUG_MAX_LENGTH).gsub(/\A-+|-+\z/, "").presence || "entry-#{entry.id}"
|
|
96
|
+
candidate = base
|
|
97
|
+
suffix = 2
|
|
98
|
+
while taken.include?(candidate)
|
|
99
|
+
tail = "-#{suffix}"
|
|
100
|
+
candidate = "#{base.first(SLUG_MAX_LENGTH - tail.length).delete_suffix("-")}#{tail}"
|
|
101
|
+
suffix += 1
|
|
102
|
+
end
|
|
103
|
+
candidate
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -59,7 +59,16 @@ module Shiplog
|
|
|
59
59
|
search_button: "rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-indigo-500",
|
|
60
60
|
search_summary: "mb-4 text-sm text-gray-500 dark:text-gray-400",
|
|
61
61
|
search_clear: "ml-2 font-medium text-indigo-600 hover:underline dark:text-indigo-400",
|
|
62
|
-
search_empty: "text-gray-500 dark:text-gray-400"
|
|
62
|
+
search_empty: "text-gray-500 dark:text-gray-400",
|
|
63
|
+
permalink: "mx-auto max-w-2xl rounded-lg border border-gray-200 bg-white p-6 dark:border-gray-800 dark:bg-gray-900",
|
|
64
|
+
permalink_back: "mt-6 inline-block text-sm font-medium text-indigo-600 hover:underline dark:text-indigo-400",
|
|
65
|
+
permalink_nav: "mt-8 flex flex-wrap justify-between gap-4 border-t border-gray-200 pt-4 dark:border-gray-800",
|
|
66
|
+
permalink_nav_link: "text-sm font-medium text-indigo-600 hover:underline dark:text-indigo-400",
|
|
67
|
+
areas: "my-4 flex flex-wrap gap-2",
|
|
68
|
+
area_link: "rounded-full border border-gray-300 px-3 py-0.5 text-sm hover:border-indigo-500 dark:border-gray-700 [&.shiplog-areas__link--current]:border-indigo-600 [&.shiplog-areas__link--current]:bg-indigo-600 [&.shiplog-areas__link--current]:text-white",
|
|
69
|
+
entry_area: "text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400",
|
|
70
|
+
entry_images: "mt-3 grid w-full gap-3",
|
|
71
|
+
entry_image: "h-auto max-w-full rounded-lg border border-gray-200 dark:border-gray-800"
|
|
63
72
|
}.freeze
|
|
64
73
|
|
|
65
74
|
def copy_initializer
|