504 lines
22 KiB
Plaintext
504 lines
22 KiB
Plaintext
<div class="app" id="bds-shell-app" phx-hook="AppShell" data-shortcuts={encoded_shortcuts(@client_shortcuts)}>
|
||
<div
|
||
class={["window-titlebar", if(@is_mac_ui, do: "is-mac")]}
|
||
data-region="title-bar"
|
||
data-testid="window-titlebar"
|
||
data-open-menu-group={@titlebar_menu_group || ""}
|
||
>
|
||
<%= unless @is_mac_ui do %>
|
||
<div class="window-titlebar-menu-bar" data-testid="window-titlebar-menu-bar">
|
||
<%= for group <- @menu_groups do %>
|
||
<button
|
||
class={[
|
||
"window-titlebar-menu-button",
|
||
if(@titlebar_menu_group == Atom.to_string(group.id), do: "is-active")
|
||
]}
|
||
data-testid="window-titlebar-menu-button"
|
||
data-menu-group={group.id}
|
||
type="button"
|
||
phx-click="toggle_titlebar_menu"
|
||
phx-mouseenter="hover_titlebar_menu"
|
||
phx-value-group={group.id}
|
||
aria-label={group.label}
|
||
><%= group.label %></button>
|
||
<% end %>
|
||
</div>
|
||
<% end %>
|
||
<div class="window-titlebar-drag-region"></div>
|
||
<div class="window-titlebar-title" data-testid="window-title"><%= @page_title %></div>
|
||
<div class="window-titlebar-actions">
|
||
<button
|
||
class="window-titlebar-action-button"
|
||
data-testid="toggle-sidebar"
|
||
type="button"
|
||
phx-click="toggle_sidebar"
|
||
aria-label="Toggle sidebar"
|
||
title="Toggle sidebar"
|
||
>
|
||
<span class={["window-titlebar-sidebar-icon", if(@workbench.sidebar_visible, do: "is-active", else: "is-inactive")]}>
|
||
<span class="window-titlebar-sidebar-pane"></span>
|
||
</span>
|
||
</button>
|
||
<button
|
||
class="window-titlebar-action-button"
|
||
data-testid="toggle-panel"
|
||
type="button"
|
||
phx-click="toggle_panel"
|
||
aria-label="Toggle panel"
|
||
title="Toggle panel"
|
||
>
|
||
<span class={["window-titlebar-panel-icon", if(@workbench.panel.visible, do: "is-active", else: "is-inactive")]}>
|
||
<span class="window-titlebar-panel-pane"></span>
|
||
</span>
|
||
</button>
|
||
<button
|
||
class="window-titlebar-action-button"
|
||
data-testid="toggle-assistant"
|
||
type="button"
|
||
phx-click="toggle_assistant_sidebar"
|
||
aria-label="Toggle assistant"
|
||
title="Toggle assistant"
|
||
>
|
||
<span class={["window-titlebar-assistant-icon", if(@workbench.assistant_sidebar_visible, do: "is-active", else: "is-inactive")]}>
|
||
<span class="window-titlebar-assistant-pane"></span>
|
||
</span>
|
||
</button>
|
||
</div>
|
||
<%= if not @is_mac_ui do %>
|
||
<%= if group = active_titlebar_menu_group(assigns) do %>
|
||
<div
|
||
class="window-titlebar-menu-dropdown"
|
||
data-testid="window-titlebar-menu-dropdown"
|
||
phx-click-away="close_titlebar_menu"
|
||
>
|
||
<%= for item <- group.items do %>
|
||
<%= if item.separator do %>
|
||
<div class="window-titlebar-menu-separator"></div>
|
||
<% else %>
|
||
<button
|
||
class="window-titlebar-menu-item"
|
||
data-testid="window-titlebar-menu-item"
|
||
data-menu-action={item.id}
|
||
type="button"
|
||
phx-click="titlebar_menu_action"
|
||
phx-value-action={item.id}
|
||
aria-label={item.label}
|
||
>
|
||
<span class="window-titlebar-menu-item-label"><%= item.label %></span>
|
||
<%= if item.shortcut do %>
|
||
<span class="window-titlebar-menu-item-accelerator"><%= item.shortcut %></span>
|
||
<% end %>
|
||
</button>
|
||
<% end %>
|
||
<% end %>
|
||
</div>
|
||
<% end %>
|
||
<% end %>
|
||
</div>
|
||
|
||
<div class="app-main">
|
||
<aside class="activity-bar" data-region="activity-bar">
|
||
<div class="activity-bar-top">
|
||
<%= for button <- Enum.filter(@activity_buttons, &(&1.activity_group == :top)) do %>
|
||
<button
|
||
class={["activity-bar-item", if(button.active, do: "active")]}
|
||
data-testid="activity-button"
|
||
data-view={button.id}
|
||
data-active={to_string(button.active)}
|
||
type="button"
|
||
phx-click="select_view"
|
||
phx-value-view={button.id}
|
||
title={activity_label(button.label)}
|
||
aria-label={activity_label(button.label)}
|
||
>
|
||
<%= raw(ShellData.activity_icon(button.id)) %>
|
||
</button>
|
||
<% end %>
|
||
</div>
|
||
<div class="activity-bar-bottom">
|
||
<%= for button <- Enum.filter(@activity_buttons, &(&1.activity_group == :bottom)) do %>
|
||
<button
|
||
class={["activity-bar-item", if(button.active, do: "active")]}
|
||
data-testid="activity-button"
|
||
data-view={button.id}
|
||
data-active={to_string(button.active)}
|
||
type="button"
|
||
phx-click="select_view"
|
||
phx-value-view={button.id}
|
||
title={activity_label(button.label)}
|
||
aria-label={activity_label(button.label)}
|
||
>
|
||
<%= raw(ShellData.activity_icon(button.id)) %>
|
||
</button>
|
||
<% end %>
|
||
</div>
|
||
</aside>
|
||
|
||
<section
|
||
class={["sidebar-shell", if(not @workbench.sidebar_visible, do: "is-hidden")]}
|
||
data-testid="sidebar-shell"
|
||
style={"width: #{if(@workbench.sidebar_visible, do: @workbench.sidebar_width, else: 0)}px;"}
|
||
>
|
||
<div class="sidebar" data-region="sidebar">
|
||
<div id="sidebar-content" class="sidebar-content sidebar-body" phx-hook="SidebarInteractions">
|
||
<div class="sidebar-section">
|
||
<div class="sidebar-section-header">
|
||
<span><%= String.upcase(sidebar_header_label(@sidebar_header)) %></span>
|
||
<%= if sidebar_filters_enabled?(@sidebar_data) do %>
|
||
<div class="sidebar-actions">
|
||
<button
|
||
class={[
|
||
"sidebar-action",
|
||
if(sidebar_filters_visible?(@sidebar_data), do: "active")
|
||
]}
|
||
data-testid="sidebar-filter-toggle"
|
||
type="button"
|
||
phx-click="toggle_sidebar_filters"
|
||
aria-label={translated(Map.get(@sidebar_data.filters, :toggle_filters_label))}
|
||
title={translated(Map.get(@sidebar_data.filters, :toggle_filters_label))}
|
||
>
|
||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||
<path d="M6 12v-1h4v1H6zM4 8v-1h8v1H4zm-2-4v-1h12v1H2z"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<% end %>
|
||
</div>
|
||
</div>
|
||
<%= render_sidebar_filters(assigns) %>
|
||
<%= render_sidebar_body(assigns) %>
|
||
<%= render_sidebar_load_more(assigns) %>
|
||
</div>
|
||
</div>
|
||
<div class="resizable-panel-divider sidebar-divider" data-resize="sidebar" data-role="resize-handle"></div>
|
||
</section>
|
||
|
||
<main class="app-content" data-region="content">
|
||
<div class="tab-bar" data-region="tab-bar">
|
||
<%= if Enum.empty?(@workbench.tabs) do %>
|
||
<div class="tab-bar-empty"><%= translated("Dashboard") %></div>
|
||
<% else %>
|
||
<div class="tab-bar-tabs">
|
||
<%= for tab <- @workbench.tabs do %>
|
||
<div
|
||
class={[
|
||
"tab",
|
||
if(@workbench.active_tab == {tab.type, tab.id}, do: "active"),
|
||
if(tab.is_transient, do: "transient"),
|
||
if(Workbench.dirty?(@workbench, tab.type, tab.id), do: "dirty")
|
||
]}
|
||
data-tab-type={tab.type}
|
||
data-tab-id={tab.id}
|
||
tabindex="0"
|
||
>
|
||
<button
|
||
class="tab-select"
|
||
type="button"
|
||
phx-click="select_tab"
|
||
phx-value-type={tab.type}
|
||
phx-value-id={tab.id}
|
||
>
|
||
<span class="tab-icon"><%= raw(ShellData.activity_icon(tab_icon_id(tab))) %></span>
|
||
<span class="tab-title"><%= tab_title(tab, @tab_meta) %></span>
|
||
</button>
|
||
<div class="tab-actions">
|
||
<%= if Workbench.dirty?(@workbench, tab.type, tab.id) do %>
|
||
<span class="tab-dirty-indicator">●</span>
|
||
<% end %>
|
||
<button
|
||
class="tab-close"
|
||
data-testid="tab-close"
|
||
data-tab-type={tab.type}
|
||
data-tab-id={tab.id}
|
||
type="button"
|
||
phx-click="close_tab"
|
||
phx-value-type={tab.type}
|
||
phx-value-id={tab.id}
|
||
aria-label={translated("Close tab")}
|
||
title={translated("Close tab")}
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<% end %>
|
||
</div>
|
||
<% end %>
|
||
</div>
|
||
|
||
<section class="editor-shell" data-region="editor">
|
||
<%= if is_nil(@current_tab) do %>
|
||
<div class="editor-empty">
|
||
<div class="dashboard-content">
|
||
<h1 data-testid="editor-title"><%= translated("dashboard.title") %></h1>
|
||
<p class="text-muted"><%= translated("dashboard.subtitle") %></p>
|
||
|
||
<div class="dashboard-stats">
|
||
<div class="stat-card">
|
||
<div class="stat-number"><%= @dashboard.post_stats.total_posts || 0 %></div>
|
||
<div class="stat-label"><%= translated("dashboard.stats.totalPosts") %></div>
|
||
<div class="stat-breakdown">
|
||
<span class="stat-tag stat-published"><%= translated("dashboard.stats.published", %{count: @dashboard.post_stats.published_count || 0}) %></span>
|
||
<span class="stat-tag stat-draft"><%= translated("dashboard.stats.drafts", %{count: @dashboard.post_stats.draft_count || 0}) %></span>
|
||
<%= if (@dashboard.post_stats.archived_count || 0) > 0 do %>
|
||
<span class="stat-tag stat-archived"><%= translated("dashboard.stats.archived", %{count: @dashboard.post_stats.archived_count || 0}) %></span>
|
||
<% end %>
|
||
</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-number"><%= @dashboard.media_stats.media_count || 0 %></div>
|
||
<div class="stat-label"><%= translated("dashboard.stats.mediaFiles") %></div>
|
||
<div class="stat-breakdown">
|
||
<span class="stat-tag"><%= translated("dashboard.stats.images", %{count: @dashboard.media_stats.image_count || 0}) %></span>
|
||
<span class="stat-tag"><%= ShellData.format_bytes(@dashboard.media_stats.total_bytes || 0) %></span>
|
||
</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-number"><%= length(@dashboard_tag_cloud_items) %></div>
|
||
<div class="stat-label"><%= translated("dashboard.stats.tags") %></div>
|
||
<div class="stat-breakdown">
|
||
<span class="stat-tag"><%= translated("dashboard.stats.categories", %{count: length(@dashboard_category_counts)}) %></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<%= if Enum.any?(@dashboard_timeline_entries) do %>
|
||
<div class="dashboard-section">
|
||
<h4><%= translated("dashboard.section.postsOverTime") %></h4>
|
||
<div class="timeline-chart">
|
||
<%= for entry <- @dashboard_timeline_entries do %>
|
||
<div class="timeline-bar-container">
|
||
<div class="timeline-bar" style={"height: #{timeline_height(entry, @dashboard_timeline_entries)}%"}>
|
||
<span class="timeline-bar-count"><%= entry.count || 0 %></span>
|
||
</div>
|
||
<div class="timeline-bar-label">
|
||
<span class="timeline-bar-label-month"><%= ShellData.format_dashboard_month(entry.year, entry.month) %></span>
|
||
<span class="timeline-bar-label-year"><%= entry.year %></span>
|
||
</div>
|
||
</div>
|
||
<% end %>
|
||
</div>
|
||
</div>
|
||
<% end %>
|
||
|
||
<%= if Enum.any?(@dashboard_tag_cloud_items) do %>
|
||
<div class="dashboard-section">
|
||
<h4><%= translated("dashboard.section.tags") %></h4>
|
||
<div class="tag-cloud">
|
||
<%= for item <- @dashboard_tag_cloud_items do %>
|
||
<span class={["dashboard-tag", if(item.color, do: "has-color")]} style={ShellData.render_dashboard_tag_style(item)} title={ShellData.dashboard_post_count_label(item.count)}><%= item.tag %></span>
|
||
<% end %>
|
||
</div>
|
||
</div>
|
||
<% end %>
|
||
|
||
<%= if Enum.any?(@dashboard_category_counts) do %>
|
||
<div class="dashboard-section">
|
||
<h4><%= translated("dashboard.section.categories") %></h4>
|
||
<div class="tag-cloud">
|
||
<%= for category <- @dashboard_category_counts do %>
|
||
<span class="dashboard-tag dashboard-category" title={ShellData.dashboard_post_count_label(category.count || 0)}>
|
||
<%= category.category || "" %>
|
||
<span class="tag-count"><%= category.count || 0 %></span>
|
||
</span>
|
||
<% end %>
|
||
</div>
|
||
</div>
|
||
<% end %>
|
||
|
||
<%= if Enum.any?(@dashboard_recent_posts) do %>
|
||
<div class="dashboard-section">
|
||
<h4><%= translated("dashboard.section.recentlyUpdated") %></h4>
|
||
<div class="recent-posts-list">
|
||
<%= for post <- @dashboard_recent_posts do %>
|
||
<button
|
||
class="recent-post-item"
|
||
data-testid="recent-post-item"
|
||
data-post-id={post.id}
|
||
type="button"
|
||
phx-click="open_sidebar_item"
|
||
phx-value-route="post"
|
||
phx-value-id={post.id}
|
||
phx-value-title={post.title || ""}
|
||
phx-value-subtitle={post.status || "draft"}
|
||
>
|
||
<span class="recent-post-title"><%= post.title || "" %></span>
|
||
<span class={"recent-post-status status-#{post.status || "draft"}"}><%= ShellData.dashboard_status_label(post.status || "draft") %></span>
|
||
<span class="recent-post-date"><%= ShellData.format_dashboard_date(post.updated_at) %></span>
|
||
</button>
|
||
<% end %>
|
||
</div>
|
||
</div>
|
||
<% end %>
|
||
|
||
<div class="dashboard-inspector-meta" hidden>
|
||
<%= for item <- @editor_meta do %>
|
||
<section class="editor-meta-row">
|
||
<strong data-testid="editor-meta-label"><%= translated(item.label) %></strong>
|
||
<span><%= translated(item.value) %></span>
|
||
</section>
|
||
<% end %>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<% else %>
|
||
<div class="editor-frame">
|
||
<section class="editor-main">
|
||
<div class="editor-kicker"><%= tab_route_label(@current_tab) %></div>
|
||
<h1 class="editor-title" data-testid="editor-title"><%= tab_title(@current_tab, @tab_meta) %></h1>
|
||
<p class="editor-subtitle"><%= tab_subtitle(@current_tab, @tab_meta) %></p>
|
||
|
||
<div class="editor-toolbar">
|
||
<button class="editor-toolbar-button" type="button">Open</button>
|
||
<button class="editor-toolbar-button" type="button">Preview</button>
|
||
<button class="editor-toolbar-button" type="button">Metadata</button>
|
||
</div>
|
||
|
||
<div class="editor-section">
|
||
<h2><%= tab_title(@current_tab, @tab_meta) %></h2>
|
||
<p>Desktop workbench content routed through the Elixir shell.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<aside class="editor-meta">
|
||
<%= for item <- @editor_meta do %>
|
||
<section class="editor-meta-row">
|
||
<strong data-testid="editor-meta-label"><%= translated(item.label) %></strong>
|
||
<span><%= translated(item.value) %></span>
|
||
</section>
|
||
<% end %>
|
||
</aside>
|
||
</div>
|
||
<% end %>
|
||
</section>
|
||
|
||
<section class={["panel-shell", if(not @workbench.panel.visible, do: "is-hidden")]} data-region="panel">
|
||
<div class="panel-header">
|
||
<div class="panel-tabs">
|
||
<%= for tab <- @panel_tabs do %>
|
||
<button
|
||
class={["panel-tab", if(@workbench.panel.active_tab == tab, do: "active")]}
|
||
type="button"
|
||
phx-click="select_panel_tab"
|
||
phx-value-tab={tab}
|
||
>
|
||
<%= panel_tab_label(tab) %>
|
||
</button>
|
||
<% end %>
|
||
</div>
|
||
<button
|
||
class="panel-close"
|
||
data-testid="panel-close"
|
||
type="button"
|
||
phx-click="toggle_panel"
|
||
aria-label={translated("Close panel")}
|
||
title={translated("Close panel")}
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
<div class="panel-content">
|
||
<%= render_panel_body(assigns) %>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
|
||
<section
|
||
class={["assistant-sidebar-shell", if(not @workbench.assistant_sidebar_visible, do: "is-hidden")]}
|
||
data-testid="assistant-shell"
|
||
style={"width: #{if(@workbench.assistant_sidebar_visible, do: @workbench.assistant_sidebar_width, else: 0)}px;"}
|
||
>
|
||
<div class="resizable-panel-divider assistant-divider" data-resize="assistant" data-role="resize-handle"></div>
|
||
<aside class="assistant-sidebar" data-region="assistant-sidebar">
|
||
<div class="assistant-header">
|
||
<strong><%= translated("Assistant") %></strong>
|
||
</div>
|
||
<div class="assistant-content">
|
||
<%= for card <- @assistant_cards do %>
|
||
<section class="assistant-card">
|
||
<strong><%= translated(card.label) %></strong>
|
||
<span><%= translated(card.text) %></span>
|
||
</section>
|
||
<% end %>
|
||
</div>
|
||
</aside>
|
||
</section>
|
||
</div>
|
||
|
||
<footer class="status-bar" data-region="status-bar" data-testid="status-bar">
|
||
<div class="status-bar-left">
|
||
<div class="project-selector">
|
||
<button
|
||
class="project-selector-trigger"
|
||
data-testid="project-selector-trigger"
|
||
type="button"
|
||
title={translated("Switch project")}
|
||
phx-click="toggle_project_menu"
|
||
>
|
||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" class="project-icon">
|
||
<path d="M14.5 3H7.71l-.85-.85A.5.5 0 0 0 6.5 2h-5a.5.5 0 0 0-.5.5v11a.5.5 0 0 0 .5.5h13a.5.5 0 0 0 .5-.5v-10a.5.5 0 0 0-.5-.5zm-13 1h5.29l.85.85c.1.1.23.15.36.15h6.5v9h-13V4z"></path>
|
||
</svg>
|
||
<span class="project-name"><%= if @current_project, do: @current_project.name, else: "My Blog" %></span>
|
||
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" class="dropdown-arrow">
|
||
<path d="M4.5 5.5L8 9l3.5-3.5h-7z"></path>
|
||
</svg>
|
||
</button>
|
||
|
||
<%= if @project_menu_open do %>
|
||
<div class="project-dropdown" data-testid="project-dropdown" phx-click-away="close_project_menu">
|
||
<div class="project-dropdown-header">
|
||
<span><%= translated("Projects") %></span>
|
||
</div>
|
||
<div class="project-list">
|
||
<%= for project <- @projects.projects do %>
|
||
<button
|
||
class={["project-item", if(project.id == @projects.active_project_id, do: "active")]}
|
||
data-testid="project-item"
|
||
data-project-id={project.id}
|
||
type="button"
|
||
phx-click="select_project"
|
||
phx-value-project_id={project.id}
|
||
>
|
||
<span class="project-item-name"><%= project.name %></span>
|
||
<%= if project.id == @projects.active_project_id do %>
|
||
<span class="project-check-icon">✓</span>
|
||
<% end %>
|
||
</button>
|
||
<% end %>
|
||
</div>
|
||
<div class="project-dropdown-footer">
|
||
<button class="existing-project-btn" type="button" phx-click="import_project">
|
||
<span><%= translated("Open Existing Blog") %></span>
|
||
</button>
|
||
<button class="create-project-btn" type="button" phx-click="create_project">
|
||
<span><%= translated("New Project") %></span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<% end %>
|
||
</div>
|
||
<button class="status-bar-item status-bar-task-button" data-testid="status-task-button" type="button" phx-click="open_tasks_panel">
|
||
<span><%= @status.left.running_task_message || translated("Idle") %></span>
|
||
<%= if (@status.left.running_task_overflow || 0) > 0 do %>
|
||
<span class="status-bar-count">+<%= @status.left.running_task_overflow %></span>
|
||
<% end %>
|
||
</button>
|
||
</div>
|
||
<div class="status-bar-right">
|
||
<span class="status-bar-item"><%= @status.right.post_count %></span>
|
||
<span class="status-bar-item"><%= @status.right.media_count %></span>
|
||
<span class="status-bar-item theme-badge"><%= @status.right.theme_badge %></span>
|
||
<button class={["status-bar-item", "offline-badge", if(@status.right.offline_mode, do: "active")]} data-testid="status-offline-button" type="button" phx-click="toggle_offline_mode" title={translated("Toggle offline mode")}>✈</button>
|
||
<form class="status-bar-item language-badge" data-testid="status-language-form" phx-change="change_ui_language">
|
||
<span><%= translated("UI") %></span>
|
||
<select class="status-bar-language-select" name="ui_language" data-testid="status-language-select">
|
||
<%= for language <- @supported_ui_languages do %>
|
||
<option selected={language.code == @page_language} value={language.code}><%= language.flag %></option>
|
||
<% end %>
|
||
</select>
|
||
</form>
|
||
<span class="status-bar-item brand"><%= @status.right.brand %></span>
|
||
</div>
|
||
</footer>
|
||
</div>
|