feat(ui): limit visible reactions and overhaul retro theme
- Limit the number of initially visible reactions per file to 2 and calculate the overflow count on the backend. - Redesign the retro theme CSS to mimic a classic Windows 98 Explorer window, including title bars, toolbars, and sunken panes. - Add local storage persistence for the file browser view preference (list vs. thumbnails).
This commit is contained in:
@@ -50,6 +50,7 @@ type fileView struct {
|
|||||||
IconRetroURL string
|
IconRetroURL string
|
||||||
ReactURL string
|
ReactURL string
|
||||||
Reactions []reactionView
|
Reactions []reactionView
|
||||||
|
ReactionMore int
|
||||||
Reacted bool
|
Reacted bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +59,7 @@ type reactionView struct {
|
|||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
Label string `json:"label"`
|
Label string `json:"label"`
|
||||||
Count int `json:"count"`
|
Count int `json:"count"`
|
||||||
|
Visible bool `json:"visible"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type emojiTabView struct {
|
type emojiTabView struct {
|
||||||
@@ -354,6 +356,7 @@ func (a *App) fileView(box services.Box, file services.File) fileView {
|
|||||||
|
|
||||||
func (a *App) fileViewWithReactions(box services.Box, file services.File, reactions []services.ReactionSummary, reacted bool) fileView {
|
func (a *App) fileViewWithReactions(box services.Box, file services.File, reactions []services.ReactionSummary, reacted bool) fileView {
|
||||||
icon := a.fileIcons.lookup(file.Name, file.ContentType)
|
icon := a.fileIcons.lookup(file.Name, file.ContentType)
|
||||||
|
reactionViews := a.reactionViews(reactions)
|
||||||
return fileView{
|
return fileView{
|
||||||
ID: file.ID,
|
ID: file.ID,
|
||||||
Name: file.Name,
|
Name: file.Name,
|
||||||
@@ -367,7 +370,8 @@ func (a *App) fileViewWithReactions(box services.Box, file services.File, reacti
|
|||||||
IconURL: fileIconURL("standard", icon.Standard),
|
IconURL: fileIconURL("standard", icon.Standard),
|
||||||
IconRetroURL: fileIconURL("retro", icon.Retro),
|
IconRetroURL: fileIconURL("retro", icon.Retro),
|
||||||
ReactURL: fmt.Sprintf("/d/%s/f/%s/react", box.ID, file.ID),
|
ReactURL: fmt.Sprintf("/d/%s/f/%s/react", box.ID, file.ID),
|
||||||
Reactions: a.reactionViews(reactions),
|
Reactions: reactionViews,
|
||||||
|
ReactionMore: reactionOverflowCount(reactionViews),
|
||||||
Reacted: reacted,
|
Reacted: reacted,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -413,17 +417,25 @@ func (a *App) ReactToFile(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func (a *App) reactionViews(reactions []services.ReactionSummary) []reactionView {
|
func (a *App) reactionViews(reactions []services.ReactionSummary) []reactionView {
|
||||||
views := make([]reactionView, 0, len(reactions))
|
views := make([]reactionView, 0, len(reactions))
|
||||||
for _, reaction := range reactions {
|
for index, reaction := range reactions {
|
||||||
views = append(views, reactionView{
|
views = append(views, reactionView{
|
||||||
EmojiID: reaction.EmojiID,
|
EmojiID: reaction.EmojiID,
|
||||||
URL: emojiURL(reaction.EmojiID),
|
URL: emojiURL(reaction.EmojiID),
|
||||||
Label: emojiLabel(reaction.EmojiID),
|
Label: emojiLabel(reaction.EmojiID),
|
||||||
Count: reaction.Count,
|
Count: reaction.Count,
|
||||||
|
Visible: index < 2,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return views
|
return views
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func reactionOverflowCount(reactions []reactionView) int {
|
||||||
|
if len(reactions) <= 2 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return len(reactions) - 2
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) emojiTabs() ([]emojiTabView, error) {
|
func (a *App) emojiTabs() ([]emojiTabView, error) {
|
||||||
root := a.emojiRoot()
|
root := a.emojiRoot()
|
||||||
entries, err := os.ReadDir(root)
|
entries, err := os.ReadDir(root)
|
||||||
|
|||||||
@@ -592,31 +592,140 @@
|
|||||||
content: "\23F1 ";
|
content: "\23F1 ";
|
||||||
}
|
}
|
||||||
|
|
||||||
/* List / Thumbnails / Preview images = a Win98 toolbar (menubar) of flat
|
/* The file browser becomes a Win98 Explorer window: blue titlebar, grey
|
||||||
buttons that raise on hover and depress when active. */
|
toolbar, sunken content pane and flat rows. */
|
||||||
|
:root[data-theme="retro"] .file-browser-window {
|
||||||
|
border: 1px solid #000000;
|
||||||
|
border-radius: 0;
|
||||||
|
background: #c0c0c0;
|
||||||
|
box-shadow: inset -1px -1px 0 #404040, inset 1px 1px 0 #ffffff, inset -2px -2px 0 #808080, inset 2px 2px 0 #dfdfdf;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="retro"] .file-browser-titlebar {
|
||||||
|
min-height: 1.8rem;
|
||||||
|
margin: 3px 3px 0;
|
||||||
|
padding: 0.2rem 0.45rem;
|
||||||
|
border: 0;
|
||||||
|
background: linear-gradient(to right, #000078 0%, #000078 80%, #0f80cd 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="retro"] .file-browser-titlebar strong,
|
||||||
|
:root[data-theme="retro"] .file-browser-titlebar span {
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="retro"] .file-browser-window-actions {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="retro"] .file-browser-toolbar {
|
||||||
|
justify-content: space-between;
|
||||||
|
margin: 0 3px;
|
||||||
|
padding: 3px;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid #808080;
|
||||||
|
background: #c0c0c0;
|
||||||
|
}
|
||||||
|
|
||||||
:root[data-theme="retro"] .view-toolbar {
|
:root[data-theme="retro"] .view-toolbar {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
margin-top: 1rem;
|
margin-top: 0;
|
||||||
padding: 3px;
|
padding: 0;
|
||||||
background: #c0c0c0;
|
background: #c0c0c0;
|
||||||
border: 1px solid #000000;
|
border: 0;
|
||||||
box-shadow: inset 1px 1px 0 #ffffff, inset -1px -1px 0 #808080;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="retro"] .view-toolbar .button {
|
:root[data-theme="retro"] .view-toolbar .button,
|
||||||
|
:root[data-theme="retro"] .file-browser-toolbar > .button {
|
||||||
|
display: inline-grid;
|
||||||
|
place-items: center;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="retro"] .view-toolbar .button:hover {
|
:root[data-theme="retro"] .view-toolbar .icon-button {
|
||||||
|
width: 2.2rem;
|
||||||
|
height: 2rem;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="retro"] .view-toolbar .icon-button svg {
|
||||||
|
margin: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="retro"] .view-toolbar .button:hover,
|
||||||
|
:root[data-theme="retro"] .file-browser-toolbar > .button:hover {
|
||||||
background: #c0c0c0;
|
background: #c0c0c0;
|
||||||
box-shadow: inset -1px -1px 0 #808080, inset 1px 1px 0 #ffffff;
|
box-shadow: inset -1px -1px 0 #808080, inset 1px 1px 0 #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-theme="retro"] .view-toolbar .button.is-active {
|
:root[data-theme="retro"] .view-toolbar .button.is-active,
|
||||||
|
:root[data-theme="retro"] .file-browser-toolbar > .button.is-active {
|
||||||
background: #d4d0c8;
|
background: #d4d0c8;
|
||||||
box-shadow: inset 1px 1px 0 #808080, inset -1px -1px 0 #ffffff;
|
box-shadow: inset 1px 1px 0 #808080, inset -1px -1px 0 #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root[data-theme="retro"] .file-browser-head {
|
||||||
|
margin: 0 3px;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid #808080;
|
||||||
|
background: #c0c0c0;
|
||||||
|
color: #000000;
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="retro"] .file-browser {
|
||||||
|
margin: 0 3px 3px;
|
||||||
|
border: 1px solid #000000;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: inset 1px 1px 0 #808080, inset -1px -1px 0 #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="retro"] .download-item {
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="retro"] .file-open {
|
||||||
|
border-radius: 0;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="retro"] .file-open:hover,
|
||||||
|
:root[data-theme="retro"] .file-open:focus-visible {
|
||||||
|
background: transparent;
|
||||||
|
color: #000000;
|
||||||
|
outline: 2px solid #000078;
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="retro"] .file-media {
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="retro"] .file-browser.is-thumbs .file-open {
|
||||||
|
align-content: start;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="retro"] .file-browser.is-thumbs .file-media {
|
||||||
|
justify-self: center;
|
||||||
|
align-self: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="retro"] .file-type,
|
||||||
|
:root[data-theme="retro"] .file-size,
|
||||||
|
:root[data-theme="retro"] .file-main small {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|||||||
@@ -46,12 +46,22 @@
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-toolbar {
|
.thumb-link {
|
||||||
display: flex;
|
flex: 0 0 4.75rem;
|
||||||
justify-content: center;
|
width: 4.75rem;
|
||||||
flex-wrap: wrap;
|
aspect-ratio: 16 / 10;
|
||||||
gap: 0.5rem;
|
display: block;
|
||||||
margin-top: 1rem;
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: calc(var(--radius) - 0.2rem);
|
||||||
|
background: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb-link img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button.is-active {
|
.button.is-active {
|
||||||
@@ -59,26 +69,148 @@
|
|||||||
color: var(--primary-foreground);
|
color: var(--primary-foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-browser-window {
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--border) 78%, var(--primary));
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, color-mix(in srgb, var(--card) 94%, transparent), color-mix(in srgb, var(--background) 92%, transparent));
|
||||||
|
box-shadow: 0 18px 54px rgba(0, 0, 0, 0.24);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-browser-titlebar {
|
||||||
|
min-height: 3rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0.75rem 0.9rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: color-mix(in srgb, var(--muted) 62%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-browser-titlebar > div:first-child {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-browser-titlebar strong {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-browser-titlebar span {
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-browser-window-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-browser-window-actions span {
|
||||||
|
width: 0.72rem;
|
||||||
|
height: 0.72rem;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--border) 75%, var(--foreground));
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-browser-toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.65rem 0.75rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: color-mix(in srgb, var(--card) 74%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-toolbar {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-toolbar .button,
|
||||||
|
.file-browser-toolbar > .button {
|
||||||
|
min-height: 2rem;
|
||||||
|
padding: 0.35rem 0.65rem;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-toolbar .icon-button {
|
||||||
|
width: 2.25rem;
|
||||||
|
padding-inline: 0;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-toolbar svg {
|
||||||
|
width: 0.95rem;
|
||||||
|
height: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-browser-head {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 3rem minmax(0, 1fr) minmax(8rem, 0.38fr) minmax(5rem, 0.18fr) minmax(8rem, 0.32fr);
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.42rem 1rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
background: color-mix(in srgb, var(--background) 78%, transparent);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-browser-head span:first-child {
|
||||||
|
grid-column: 2;
|
||||||
|
}
|
||||||
|
|
||||||
.file-browser {
|
.file-browser {
|
||||||
|
display: grid;
|
||||||
|
gap: 0;
|
||||||
|
padding: 0.35rem;
|
||||||
transition: opacity 160ms ease;
|
transition: opacity 160ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-browser .download-item {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
border: 0;
|
||||||
|
border-radius: calc(var(--radius) - 0.25rem);
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 0;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-browser .download-item:hover {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
.file-card {
|
.file-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-block: 0.65rem 2.6rem;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-reaction-dock {
|
.file-reaction-dock {
|
||||||
position: absolute;
|
position: static;
|
||||||
right: 0.65rem;
|
|
||||||
bottom: 0.55rem;
|
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
max-width: calc(100% - 1.3rem);
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
gap: 0.35rem;
|
gap: 0.35rem;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
padding-right: 0.65rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-reactions {
|
.file-reactions {
|
||||||
@@ -87,10 +219,13 @@
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reaction-pill {
|
.reaction-pill {
|
||||||
|
appearance: none;
|
||||||
|
flex: 0 0 auto;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.2rem;
|
gap: 0.2rem;
|
||||||
@@ -104,6 +239,11 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.24);
|
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.24);
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-pill.is-hidden-summary {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reaction-pill img {
|
.reaction-pill img {
|
||||||
@@ -112,6 +252,31 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reaction-more {
|
||||||
|
appearance: none;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-height: 1.6rem;
|
||||||
|
padding: 0.16rem 0.45rem;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--border) 84%, var(--primary));
|
||||||
|
border-radius: 999px;
|
||||||
|
background: color-mix(in srgb, var(--card) 88%, #000);
|
||||||
|
color: var(--foreground);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 800;
|
||||||
|
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.24);
|
||||||
|
pointer-events: auto;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-pill:hover,
|
||||||
|
.reaction-pill:focus-visible,
|
||||||
|
.reaction-more:hover,
|
||||||
|
.reaction-more:focus-visible {
|
||||||
|
border-color: var(--primary);
|
||||||
|
background: var(--primary);
|
||||||
|
color: var(--primary-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
.reaction-button {
|
.reaction-button {
|
||||||
width: 2.1rem;
|
width: 2.1rem;
|
||||||
height: 2.1rem;
|
height: 2.1rem;
|
||||||
@@ -212,6 +377,30 @@ html.reaction-picker-open body {
|
|||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reaction-existing {
|
||||||
|
padding: 0.55rem 0.7rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-existing small,
|
||||||
|
.reaction-readonly-note {
|
||||||
|
display: block;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
font-size: 0.74rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-existing-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.35rem;
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-readonly-note {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.55rem 0.7rem 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
.reaction-picker-tabs {
|
.reaction-picker-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.35rem;
|
gap: 0.35rem;
|
||||||
@@ -309,11 +498,19 @@ html.reaction-picker-open body {
|
|||||||
.file-open {
|
.file-open {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: grid;
|
||||||
|
grid-template-columns: 3rem minmax(0, 1fr) minmax(8rem, 0.38fr) minmax(5rem, 0.18fr);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.8rem;
|
gap: 0.75rem;
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
padding: 0.55rem 0.65rem;
|
||||||
|
border-radius: calc(var(--radius) - 0.25rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-open:hover,
|
||||||
|
.file-open:focus-visible {
|
||||||
|
background: var(--surface-1-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-media {
|
.file-media {
|
||||||
@@ -360,11 +557,14 @@ html.reaction-picker-open body {
|
|||||||
.file-main {
|
.file-main {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
flex: 1;
|
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.file-main small {
|
.file-main small {
|
||||||
display: block;
|
display: block;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -372,44 +572,101 @@ html.reaction-picker-open body {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-type,
|
||||||
|
.file-size {
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-size {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
.file-browser.is-thumbs {
|
.file-browser.is-thumbs {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
|
gap: 0.75rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(8.75rem, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-browser-window.is-icon-view .file-browser-head {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-browser.is-thumbs .file-card {
|
.file-browser.is-thumbs .file-card {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
min-height: 13.75rem;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
align-content: start;
|
align-content: start;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-browser.is-list .file-card {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(8rem, 0.32fr);
|
||||||
|
align-items: center;
|
||||||
|
min-height: 4.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-browser.is-list .file-card:hover,
|
||||||
|
.file-browser.is-list .file-card:focus-within {
|
||||||
|
background: var(--surface-1-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-browser.is-list .file-card:hover .file-open,
|
||||||
|
.file-browser.is-list .file-card:focus-within .file-open {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
.file-browser.is-thumbs .file-open {
|
.file-browser.is-thumbs .file-open {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.55rem;
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: 6.75rem auto;
|
||||||
|
gap: 1rem;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0.65rem 0.65rem 3.05rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
|
align-content: start;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-browser.is-thumbs .file-media {
|
.file-browser.is-thumbs .file-media {
|
||||||
width: 100%;
|
width: min(6.75rem, 76%);
|
||||||
height: auto;
|
height: 6.75rem;
|
||||||
flex-basis: auto;
|
flex-basis: auto;
|
||||||
aspect-ratio: 16 / 10;
|
aspect-ratio: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-browser.is-thumbs .file-icon {
|
.file-browser.is-thumbs .file-icon {
|
||||||
width: 45%;
|
width: 64%;
|
||||||
height: auto;
|
height: 64%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-browser.is-thumbs .file-main {
|
.file-browser.is-thumbs .file-main {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0.25rem;
|
||||||
|
align-self: start;
|
||||||
|
padding-top: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-browser.images-only .file-card:not([data-kind="image"]) {
|
.file-browser.is-thumbs .file-type,
|
||||||
|
.file-browser.is-thumbs .file-size {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-browser.is-thumbs .file-reaction-dock {
|
||||||
|
position: absolute;
|
||||||
|
right: 0.6rem;
|
||||||
|
bottom: 0.65rem;
|
||||||
|
max-width: calc(100% - 1.2rem);
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.context-menu {
|
.context-menu {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 30;
|
z-index: 30;
|
||||||
|
|||||||
@@ -95,6 +95,41 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-browser-toolbar {
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-browser-toolbar,
|
||||||
|
.file-browser-toolbar .view-toolbar {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-browser-toolbar .view-toolbar .button,
|
||||||
|
.file-browser-toolbar > .button {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-browser-toolbar .view-toolbar .icon-button {
|
||||||
|
flex: 0 0 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-browser-head {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-open {
|
||||||
|
grid-template-columns: 3rem minmax(0, 1fr) auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-type {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-browser.is-list .file-card {
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(7rem, auto);
|
||||||
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 1.65rem;
|
font-size: 1.65rem;
|
||||||
}
|
}
|
||||||
@@ -213,6 +248,54 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-browser-titlebar {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-browser-titlebar > div:first-child {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-browser {
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-open {
|
||||||
|
grid-template-columns: 2.65rem minmax(0, 1fr);
|
||||||
|
gap: 0.55rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-media {
|
||||||
|
width: 2.65rem;
|
||||||
|
height: 2.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-size {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-browser.is-list .file-card {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-browser.is-list .file-reaction-dock {
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 0 0.5rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-browser.is-thumbs {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-browser.is-thumbs .file-open {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.file-actions,
|
.file-actions,
|
||||||
.file-browser.is-thumbs .file-actions {
|
.file-browser.is-thumbs .file-actions {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -1,30 +1,25 @@
|
|||||||
(function () {
|
(function () {
|
||||||
const fileBrowser = document.querySelector("[data-file-browser]");
|
const fileBrowser = document.querySelector("[data-file-browser]");
|
||||||
const viewButtons = document.querySelectorAll("[data-view-button]");
|
const viewButtons = document.querySelectorAll("[data-view-button]");
|
||||||
const previewImages = document.querySelector("[data-preview-images]");
|
|
||||||
const previewActions = document.querySelectorAll("[data-preview-action]");
|
const previewActions = document.querySelectorAll("[data-preview-action]");
|
||||||
const fileContextMenu = document.querySelector("[data-file-context-menu]");
|
const fileContextMenu = document.querySelector("[data-file-context-menu]");
|
||||||
|
const fileBrowserWindow = document.querySelector("[data-file-browser-window]");
|
||||||
|
|
||||||
let ctrlCopyMode = false;
|
let ctrlCopyMode = false;
|
||||||
let contextFile = null;
|
let contextFile = null;
|
||||||
const contextMenuCloseDistance = 80;
|
const contextMenuCloseDistance = 80;
|
||||||
|
const viewStorageKey = "warpbox.fileBrowser.view";
|
||||||
|
|
||||||
if (fileBrowser) {
|
if (fileBrowser) {
|
||||||
|
applySavedFileBrowserPreferences();
|
||||||
|
|
||||||
viewButtons.forEach((button) => {
|
viewButtons.forEach((button) => {
|
||||||
button.addEventListener("click", () => {
|
button.addEventListener("click", () => {
|
||||||
const view = button.getAttribute("data-view-button");
|
const view = button.getAttribute("data-view-button");
|
||||||
fileBrowser.classList.toggle("is-list", view === "list");
|
setFileBrowserView(view);
|
||||||
fileBrowser.classList.toggle("is-thumbs", view === "thumbs");
|
savePreference(viewStorageKey, view);
|
||||||
viewButtons.forEach((item) => item.classList.toggle("is-active", item === button));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (previewImages) {
|
|
||||||
previewImages.addEventListener("click", () => {
|
|
||||||
fileBrowser.classList.toggle("images-only");
|
|
||||||
previewImages.classList.toggle("is-active");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileBrowser && fileContextMenu) {
|
if (fileBrowser && fileContextMenu) {
|
||||||
@@ -188,4 +183,40 @@
|
|||||||
y >= rect.top - contextMenuCloseDistance &&
|
y >= rect.top - contextMenuCloseDistance &&
|
||||||
y <= rect.bottom + contextMenuCloseDistance;
|
y <= rect.bottom + contextMenuCloseDistance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applySavedFileBrowserPreferences() {
|
||||||
|
const savedView = readPreference(viewStorageKey);
|
||||||
|
setFileBrowserView(savedView === "list" ? "list" : "thumbs");
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFileBrowserView(view) {
|
||||||
|
const normalized = view === "thumbs" ? "thumbs" : "list";
|
||||||
|
fileBrowser.classList.toggle("is-list", normalized === "list");
|
||||||
|
fileBrowser.classList.toggle("is-thumbs", normalized === "thumbs");
|
||||||
|
if (fileBrowserWindow) {
|
||||||
|
fileBrowserWindow.classList.toggle("is-list-view", normalized === "list");
|
||||||
|
fileBrowserWindow.classList.toggle("is-icon-view", normalized === "thumbs");
|
||||||
|
}
|
||||||
|
viewButtons.forEach((item) => {
|
||||||
|
const active = item.getAttribute("data-view-button") === normalized;
|
||||||
|
item.classList.toggle("is-active", active);
|
||||||
|
item.setAttribute("aria-pressed", active ? "true" : "false");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function readPreference(key) {
|
||||||
|
try {
|
||||||
|
return window.localStorage.getItem(key);
|
||||||
|
} catch (_) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function savePreference(key, value) {
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(key, value);
|
||||||
|
} catch (_) {
|
||||||
|
// LocalStorage can be unavailable in private or locked-down browsers.
|
||||||
|
}
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
const panel = picker ? picker.querySelector(".reaction-picker-panel") : null;
|
const panel = picker ? picker.querySelector(".reaction-picker-panel") : null;
|
||||||
const search = picker ? picker.querySelector("[data-reaction-search]") : null;
|
const search = picker ? picker.querySelector("[data-reaction-search]") : null;
|
||||||
const closeButton = picker ? picker.querySelector("[data-reaction-close]") : null;
|
const closeButton = picker ? picker.querySelector("[data-reaction-close]") : null;
|
||||||
|
const existingSection = picker ? picker.querySelector("[data-reaction-existing]") : null;
|
||||||
|
const existingList = picker ? picker.querySelector("[data-reaction-existing-list]") : null;
|
||||||
|
const readonlyNote = picker ? picker.querySelector("[data-reaction-readonly]") : null;
|
||||||
|
const chooserElements = picker ? Array.from(picker.querySelectorAll(".reaction-picker-tabs, .reaction-search, .reaction-grid-wrap")) : [];
|
||||||
const tabs = picker ? Array.from(picker.querySelectorAll("[data-reaction-tab]")) : [];
|
const tabs = picker ? Array.from(picker.querySelectorAll("[data-reaction-tab]")) : [];
|
||||||
const panels = picker ? Array.from(picker.querySelectorAll("[data-reaction-panel]")) : [];
|
const panels = picker ? Array.from(picker.querySelectorAll("[data-reaction-panel]")) : [];
|
||||||
|
|
||||||
@@ -17,6 +21,36 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.addEventListener("click", (event) => {
|
||||||
|
const pill = event.target.closest("[data-reaction-pill]");
|
||||||
|
if (pill) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
const card = pill.closest("[data-reaction-card]") || activeCard;
|
||||||
|
if (!card) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (card.dataset.reacted === "true") {
|
||||||
|
openPickerForCard(card, pill);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
submitReactionForCard(card, pill.dataset.reactionEmojiId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const more = event.target.closest("[data-reaction-more]");
|
||||||
|
if (!more) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
const card = more.closest("[data-reaction-card]");
|
||||||
|
if (card) {
|
||||||
|
openPickerForCard(card, more);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (!picker || !panel) {
|
if (!picker || !panel) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -35,10 +69,10 @@
|
|||||||
|
|
||||||
panel.addEventListener("click", async (event) => {
|
panel.addEventListener("click", async (event) => {
|
||||||
const emoji = event.target.closest("[data-emoji-id]");
|
const emoji = event.target.closest("[data-emoji-id]");
|
||||||
if (!emoji || !activeButton || !activeCard) {
|
if (!emoji || !activeCard || activeCard.dataset.reacted === "true") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await submitReaction(emoji);
|
await submitReactionForCard(activeCard, emoji.dataset.emojiId);
|
||||||
});
|
});
|
||||||
|
|
||||||
tabs.forEach((tab) => {
|
tabs.forEach((tab) => {
|
||||||
@@ -62,6 +96,9 @@
|
|||||||
if (panel.contains(event.target) || event.target.closest("[data-reaction-button]")) {
|
if (panel.contains(event.target) || event.target.closest("[data-reaction-button]")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (event.target.closest("[data-reaction-more]") || event.target.closest("[data-reaction-pill]")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
closePicker();
|
closePicker();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -78,15 +115,24 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
function openPicker(button) {
|
function openPicker(button) {
|
||||||
activeButton = button;
|
openPickerForCard(button.closest("[data-reaction-card]"), button);
|
||||||
activeCard = button.closest("[data-reaction-card]");
|
}
|
||||||
|
|
||||||
|
function openPickerForCard(card, trigger) {
|
||||||
|
if (!card) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
activeButton = trigger || card.querySelector("[data-reaction-button]");
|
||||||
|
activeCard = card;
|
||||||
|
populateExistingReactions(card);
|
||||||
|
setPickerReadonly(card.dataset.reacted === "true");
|
||||||
picker.hidden = false;
|
picker.hidden = false;
|
||||||
picker.classList.add("is-open");
|
picker.classList.add("is-open");
|
||||||
if (search) {
|
if (search) {
|
||||||
search.value = "";
|
search.value = "";
|
||||||
filterEmoji("");
|
filterEmoji("");
|
||||||
}
|
}
|
||||||
positionPicker(button);
|
positionPicker(activeButton || card);
|
||||||
}
|
}
|
||||||
|
|
||||||
function closePicker() {
|
function closePicker() {
|
||||||
@@ -95,6 +141,7 @@
|
|||||||
document.documentElement.classList.remove("reaction-picker-open");
|
document.documentElement.classList.remove("reaction-picker-open");
|
||||||
picker.style.left = "";
|
picker.style.left = "";
|
||||||
picker.style.top = "";
|
picker.style.top = "";
|
||||||
|
setPickerReadonly(false);
|
||||||
activeButton = null;
|
activeButton = null;
|
||||||
activeCard = null;
|
activeCard = null;
|
||||||
}
|
}
|
||||||
@@ -146,12 +193,18 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitReaction(emoji) {
|
async function submitReactionForCard(card, emojiID) {
|
||||||
|
if (!card || !emojiID || card.dataset.reacted === "true") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const body = new URLSearchParams();
|
const body = new URLSearchParams();
|
||||||
body.set("emoji_id", emoji.dataset.emojiId);
|
body.set("emoji_id", emojiID);
|
||||||
|
|
||||||
activeButton.disabled = true;
|
const reactButton = card.querySelector("[data-reaction-button]");
|
||||||
const response = await fetch(activeButton.dataset.reactUrl, {
|
if (reactButton) {
|
||||||
|
reactButton.disabled = true;
|
||||||
|
}
|
||||||
|
const response = await fetch(card.dataset.reactUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
@@ -161,14 +214,19 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
activeButton.disabled = false;
|
if (reactButton) {
|
||||||
|
reactButton.disabled = false;
|
||||||
|
}
|
||||||
closePicker();
|
closePicker();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = await response.json();
|
const payload = await response.json();
|
||||||
renderReactions(activeCard, payload.reactions || []);
|
renderReactions(card, payload.reactions || []);
|
||||||
activeButton.remove();
|
card.dataset.reacted = "true";
|
||||||
|
if (reactButton) {
|
||||||
|
reactButton.remove();
|
||||||
|
}
|
||||||
closePicker();
|
closePicker();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,20 +237,68 @@
|
|||||||
}
|
}
|
||||||
list.replaceChildren();
|
list.replaceChildren();
|
||||||
reactions.forEach((reaction) => {
|
reactions.forEach((reaction) => {
|
||||||
const pill = document.createElement("span");
|
const pill = buildReactionPill(reaction);
|
||||||
pill.className = "reaction-pill";
|
if (!reaction.visible) {
|
||||||
pill.title = reaction.label || reaction.emojiId;
|
pill.classList.add("is-hidden-summary");
|
||||||
|
}
|
||||||
const image = document.createElement("img");
|
|
||||||
image.src = reaction.url;
|
|
||||||
image.alt = reaction.label || reaction.emojiId;
|
|
||||||
image.loading = "lazy";
|
|
||||||
|
|
||||||
const count = document.createElement("span");
|
|
||||||
count.textContent = reaction.count;
|
|
||||||
|
|
||||||
pill.append(image, count);
|
|
||||||
list.append(pill);
|
list.append(pill);
|
||||||
});
|
});
|
||||||
|
const hiddenCount = reactions.length > 2 ? reactions.length - 2 : 0;
|
||||||
|
if (hiddenCount > 0) {
|
||||||
|
const more = document.createElement("button");
|
||||||
|
more.className = "reaction-more";
|
||||||
|
more.type = "button";
|
||||||
|
more.dataset.reactionMore = "";
|
||||||
|
more.textContent = `+${hiddenCount}`;
|
||||||
|
more.setAttribute("aria-label", `Show ${hiddenCount} more reactions`);
|
||||||
|
list.append(more);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildReactionPill(reaction) {
|
||||||
|
const pill = document.createElement("button");
|
||||||
|
pill.className = "reaction-pill";
|
||||||
|
pill.type = "button";
|
||||||
|
pill.title = reaction.label || reaction.emojiId;
|
||||||
|
pill.dataset.reactionPill = "";
|
||||||
|
pill.dataset.reactionEmojiId = reaction.emojiId;
|
||||||
|
pill.dataset.reactionLabel = reaction.label || reaction.emojiId;
|
||||||
|
pill.dataset.reactionUrl = reaction.url;
|
||||||
|
pill.dataset.reactionCount = reaction.count;
|
||||||
|
pill.setAttribute("aria-label", `React with ${reaction.label || reaction.emojiId}`);
|
||||||
|
|
||||||
|
const image = document.createElement("img");
|
||||||
|
image.src = reaction.url;
|
||||||
|
image.alt = reaction.label || reaction.emojiId;
|
||||||
|
image.loading = "lazy";
|
||||||
|
|
||||||
|
const count = document.createElement("span");
|
||||||
|
count.textContent = reaction.count;
|
||||||
|
|
||||||
|
pill.append(image, count);
|
||||||
|
return pill;
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateExistingReactions(card) {
|
||||||
|
if (!existingSection || !existingList) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
existingList.replaceChildren();
|
||||||
|
card.querySelectorAll("[data-reaction-pill]").forEach((pill) => {
|
||||||
|
const clone = pill.cloneNode(true);
|
||||||
|
clone.classList.remove("is-hidden-summary");
|
||||||
|
existingList.append(clone);
|
||||||
|
});
|
||||||
|
existingSection.hidden = existingList.children.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPickerReadonly(readonly) {
|
||||||
|
picker.classList.toggle("is-readonly", readonly);
|
||||||
|
chooserElements.forEach((element) => {
|
||||||
|
element.hidden = readonly;
|
||||||
|
});
|
||||||
|
if (readonlyNote) {
|
||||||
|
readonlyNote.hidden = !readonly;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -45,48 +45,75 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<div class="view-toolbar" aria-label="File view options">
|
<div class="file-browser-window" data-file-browser-window>
|
||||||
<button class="button button-outline is-active" type="button" data-view-button="list">List</button>
|
<div class="file-browser-titlebar">
|
||||||
<button class="button button-outline" type="button" data-view-button="thumbs">Thumbnails</button>
|
<div>
|
||||||
<button class="button button-outline" type="button" data-preview-images>Preview images only</button>
|
<strong>File Browser</strong>
|
||||||
</div>
|
<span>{{len .Data.Files}} item{{if ne (len .Data.Files) 1}}s{{end}}</span>
|
||||||
|
</div>
|
||||||
<div class="download-list file-browser is-list" data-file-browser>
|
<div class="file-browser-window-actions" aria-hidden="true">
|
||||||
{{range .Data.Files}}
|
<span></span><span></span><span></span>
|
||||||
<article class="download-item file-card" data-kind="{{.PreviewKind}}" data-file-context data-preview-url="{{.URL}}" data-view-url="{{.DownloadURL}}?inline=1" data-download-url="{{.DownloadURL}}" data-file-name="{{.Name}}" data-reaction-card>
|
</div>
|
||||||
<a class="file-open" href="{{.DownloadURL}}?inline=1"{{if not $single}} target="_blank" rel="noopener noreferrer"{{end}} aria-label="Open {{.Name}}">
|
</div>
|
||||||
<span class="file-media">
|
<div class="file-browser-toolbar" aria-label="File view options">
|
||||||
{{if .HasThumbnail}}
|
<div class="view-toolbar">
|
||||||
<img class="file-thumb" src="{{.ThumbnailURL}}" alt="" loading="lazy">
|
<button class="button button-outline icon-button" type="button" data-view-button="list" aria-pressed="false" aria-label="List view" title="List view">
|
||||||
{{else}}
|
<svg viewBox="0 0 24 24" role="img" focusable="false" aria-hidden="true"><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01" /></svg>
|
||||||
{{if .IconURL}}<img class="file-icon file-icon-standard" src="{{.IconURL}}" alt="" loading="lazy">{{end}}
|
<span class="sr-only">List view</span>
|
||||||
{{if .IconRetroURL}}<img class="file-icon file-icon-retro" src="{{.IconRetroURL}}" alt="" loading="lazy">{{end}}
|
</button>
|
||||||
{{end}}
|
<button class="button button-outline icon-button is-active" type="button" data-view-button="thumbs" aria-pressed="true" aria-label="Icon view" title="Icon view">
|
||||||
</span>
|
<svg viewBox="0 0 24 24" role="img" focusable="false" aria-hidden="true"><rect x="3" y="3" width="7" height="7" rx="1" /><rect x="14" y="3" width="7" height="7" rx="1" /><rect x="3" y="14" width="7" height="7" rx="1" /><rect x="14" y="14" width="7" height="7" rx="1" /></svg>
|
||||||
<span class="file-main">
|
<span class="sr-only">Icon view</span>
|
||||||
<strong class="file-name" title="{{.Name}}">{{.Name}}</strong>
|
</button>
|
||||||
<small>{{.Size}} · {{.ContentType}}</small>
|
</div>
|
||||||
</span>
|
</div>
|
||||||
</a>
|
<div class="file-browser-head" aria-hidden="true">
|
||||||
{{if not $.Data.Locked}}
|
<span>Name</span>
|
||||||
<div class="file-reaction-dock" data-reaction-dock>
|
<span>Type</span>
|
||||||
<div class="file-reactions" data-reaction-list>
|
<span>Size</span>
|
||||||
{{range .Reactions}}
|
</div>
|
||||||
<span class="reaction-pill" title="{{.Label}}">
|
<div class="download-list file-browser is-thumbs" data-file-browser>
|
||||||
<img src="{{.URL}}" alt="{{.Label}}" loading="lazy">
|
{{range .Data.Files}}
|
||||||
<span>{{.Count}}</span>
|
<article class="download-item file-card" data-kind="{{.PreviewKind}}" data-file-context data-preview-url="{{.URL}}" data-view-url="{{.DownloadURL}}?inline=1" data-download-url="{{.DownloadURL}}" data-file-name="{{.Name}}" data-reaction-card data-react-url="{{.ReactURL}}" data-reacted="{{if .Reacted}}true{{else}}false{{end}}">
|
||||||
</span>
|
<a class="file-open" href="{{.DownloadURL}}?inline=1"{{if not $single}} target="_blank" rel="noopener noreferrer"{{end}} aria-label="Open {{.Name}}">
|
||||||
|
<span class="file-media">
|
||||||
|
{{if .HasThumbnail}}
|
||||||
|
<img class="file-thumb" src="{{.ThumbnailURL}}" alt="" loading="lazy">
|
||||||
|
{{else}}
|
||||||
|
{{if .IconURL}}<img class="file-icon file-icon-standard" src="{{.IconURL}}" alt="" loading="lazy">{{end}}
|
||||||
|
{{if .IconRetroURL}}<img class="file-icon file-icon-retro" src="{{.IconRetroURL}}" alt="" loading="lazy">{{end}}
|
||||||
|
{{end}}
|
||||||
|
</span>
|
||||||
|
<span class="file-main">
|
||||||
|
<strong class="file-name" title="{{.Name}}">{{.Name}}</strong>
|
||||||
|
<small>{{.Size}} · {{.ContentType}}</small>
|
||||||
|
</span>
|
||||||
|
<span class="file-type">{{.ContentType}}</span>
|
||||||
|
<span class="file-size">{{.Size}}</span>
|
||||||
|
</a>
|
||||||
|
{{if not $.Data.Locked}}
|
||||||
|
<div class="file-reaction-dock" data-reaction-dock>
|
||||||
|
<div class="file-reactions" data-reaction-list>
|
||||||
|
{{range .Reactions}}
|
||||||
|
<button class="reaction-pill {{if not .Visible}}is-hidden-summary{{end}}" type="button" title="{{.Label}}" data-reaction-pill data-reaction-emoji-id="{{.EmojiID}}" data-reaction-label="{{.Label}}" data-reaction-url="{{.URL}}" data-reaction-count="{{.Count}}" aria-label="{{if $.Data.Locked}}Reaction{{else}}React with {{.Label}}{{end}}">
|
||||||
|
<img src="{{.URL}}" alt="{{.Label}}" loading="lazy">
|
||||||
|
<span>{{.Count}}</span>
|
||||||
|
</button>
|
||||||
|
{{end}}
|
||||||
|
{{if .ReactionMore}}
|
||||||
|
<button class="reaction-more" type="button" data-reaction-more aria-label="Show all reactions">+{{.ReactionMore}}</button>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{if not .Reacted}}
|
||||||
|
<button class="reaction-button" type="button" data-reaction-button data-react-url="{{.ReactURL}}" aria-label="React to {{.Name}}" title="React">
|
||||||
|
<svg viewBox="0 0 24 24" role="img" focusable="false" aria-hidden="true"><path d="M12 21a9 9 0 1 0-9-9 9 9 0 0 0 9 9Z" /><path d="M8 14s1.4 2 4 2 4-2 4-2" /><path d="M9 9h.01M15 9h.01" /></svg>
|
||||||
|
</button>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{if not .Reacted}}
|
{{end}}
|
||||||
<button class="reaction-button" type="button" data-reaction-button data-react-url="{{.ReactURL}}" aria-label="React to {{.Name}}" title="React">
|
</article>
|
||||||
<svg viewBox="0 0 24 24" role="img" focusable="false" aria-hidden="true"><path d="M12 21a9 9 0 1 0-9-9 9 9 0 0 0 9 9Z" /><path d="M8 14s1.4 2 4 2 4-2 4-2" /><path d="M9 9h.01M15 9h.01" /></svg>
|
{{end}}
|
||||||
</button>
|
</div>
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</article>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
</div>
|
||||||
{{if not .Data.Locked}}
|
{{if not .Data.Locked}}
|
||||||
<div class="reaction-picker" data-reaction-picker hidden>
|
<div class="reaction-picker" data-reaction-picker hidden>
|
||||||
@@ -95,6 +122,11 @@
|
|||||||
<strong>React</strong>
|
<strong>React</strong>
|
||||||
<button class="button button-ghost reaction-picker-close" type="button" data-reaction-close aria-label="Close reaction picker">Close</button>
|
<button class="button button-ghost reaction-picker-close" type="button" data-reaction-close aria-label="Close reaction picker">Close</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="reaction-existing" data-reaction-existing hidden>
|
||||||
|
<small>Existing reactions</small>
|
||||||
|
<div class="reaction-existing-list" data-reaction-existing-list></div>
|
||||||
|
</div>
|
||||||
|
<p class="reaction-readonly-note" data-reaction-readonly hidden>You already reacted to this file.</p>
|
||||||
<div class="reaction-picker-tabs" role="tablist" aria-label="Emoji themes">
|
<div class="reaction-picker-tabs" role="tablist" aria-label="Emoji themes">
|
||||||
{{range $index, $tab := .Data.EmojiTabs}}
|
{{range $index, $tab := .Data.EmojiTabs}}
|
||||||
<button type="button" class="reaction-tab {{if eq $index 0}}is-active{{end}}" data-reaction-tab="{{$tab.ID}}" role="tab" aria-selected="{{if eq $index 0}}true{{else}}false{{end}}">{{$tab.Label}}</button>
|
<button type="button" class="reaction-tab {{if eq $index 0}}is-active{{end}}" data-reaction-tab="{{$tab.ID}}" role="tab" aria-selected="{{if eq $index 0}}true{{else}}false{{end}}">{{$tab.Label}}</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user