mirror of
https://github.com/svemagie/indiekit-endpoint-activitypub.git
synced 2026-04-02 15:44:58 +02:00
27 issues fixed from multi-dimensional code review (4 Critical, 6 High, 11 Medium, 6 Low): Security (Critical): - Escape HTML in OAuth authorization page to prevent XSS (C1) - Add CSRF protection to OAuth authorize flow (C2) - Replace bypassable regex sanitizer with sanitize-html library (C3) - Enforce OAuth scopes on all Mastodon API routes (C4) Security (Medium/Low): - Fix SSRF via DNS resolution before private IP check (M1) - Add rate limiting to API, auth, and app registration endpoints (M2) - Validate redirect_uri on POST /oauth/authorize (M4) - Fix custom emoji URL injection with scheme validation + escaping (M5) - Remove data: scheme from allowed image sources (L6) - Add access token expiry (1hr) and refresh token rotation (90d) (M3) - Hash client secrets before storage (L3) Architecture: - Extract batch-broadcast.js — shared delivery logic (H1a) - Extract init-indexes.js — MongoDB index creation (H1b) - Extract syndicator.js — syndication logic (H1c) - Create federation-actions.js facade for controllers (M6) - index.js reduced from 1810 to ~1169 lines (35%) Performance: - Cache moderation data with 30s TTL + write invalidation (H6) - Increase inbox queue throughput to 10 items/sec (H5) - Make account enrichment non-blocking with fire-and-forget (H4) - Remove ephemeral getReplies/getLikes/getShares from ingest (M11) - Fix LRU caches to use true LRU eviction (L1) - Fix N+1 backfill queries with batch $in lookup (L2) UI/UX: - Split 3441-line reader.css into 15 feature-scoped files (H2) - Extract inline Alpine.js interaction component (H3) - Reduce sidebar navigation from 7 to 3 items (M7) - Add ARIA live regions for dynamic content updates (M8) - Extract shared CW/non-CW content partial (M9) - Document form handling pattern convention (M10) - Add accessible labels to functional emoji icons (L4) - Convert profile editor to Alpine.js (L5) Audit: documentation-central/audits/2026-03-24-activitypub-code-review.md Plan: documentation-central/plans/2026-03-24-activitypub-audit-fixes.md
531 lines
11 KiB
CSS
531 lines
11 KiB
CSS
/* ==========================================================================
|
||
Explore Page
|
||
========================================================================== */
|
||
|
||
.ap-explore-header {
|
||
margin-bottom: var(--space-m);
|
||
}
|
||
|
||
.ap-explore-header__title {
|
||
font-size: var(--font-size-xl);
|
||
margin: 0 0 var(--space-xs);
|
||
}
|
||
|
||
.ap-explore-header__desc {
|
||
color: var(--color-on-offset);
|
||
font-size: var(--font-size-s);
|
||
margin: 0;
|
||
}
|
||
|
||
.ap-explore-form {
|
||
background: var(--color-offset);
|
||
border: var(--border-width-thin) solid var(--color-outline);
|
||
border-radius: var(--border-radius-small);
|
||
margin-bottom: var(--space-m);
|
||
padding: var(--space-m);
|
||
}
|
||
|
||
.ap-explore-form__row {
|
||
align-items: center;
|
||
display: flex;
|
||
gap: var(--space-s);
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.ap-explore-form__input {
|
||
border: var(--border-width-thin) solid var(--color-outline);
|
||
border-radius: var(--border-radius-small);
|
||
box-sizing: border-box;
|
||
font-size: var(--font-size-m);
|
||
min-width: 0;
|
||
padding: var(--space-xs) var(--space-s);
|
||
width: 100%;
|
||
}
|
||
|
||
.ap-explore-form__scope {
|
||
display: flex;
|
||
gap: var(--space-s);
|
||
}
|
||
|
||
.ap-explore-form__scope-label {
|
||
align-items: center;
|
||
cursor: pointer;
|
||
display: flex;
|
||
font-size: var(--font-size-s);
|
||
gap: var(--space-xs);
|
||
}
|
||
|
||
.ap-explore-form__btn {
|
||
background: var(--color-primary);
|
||
border: none;
|
||
border-radius: var(--border-radius-small);
|
||
color: var(--color-on-primary);
|
||
cursor: pointer;
|
||
font-size: var(--font-size-s);
|
||
padding: var(--space-xs) var(--space-m);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.ap-explore-form__btn:hover {
|
||
opacity: 0.85;
|
||
}
|
||
|
||
.ap-explore-error {
|
||
background: color-mix(in srgb, var(--color-error) 10%, transparent);
|
||
border: var(--border-width-thin) solid var(--color-error);
|
||
border-radius: var(--border-radius-small);
|
||
color: var(--color-error);
|
||
margin-bottom: var(--space-m);
|
||
padding: var(--space-s) var(--space-m);
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
.ap-explore-form__row {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.ap-explore-form__btn {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
/* ---------- Autocomplete dropdown ---------- */
|
||
|
||
.ap-explore-autocomplete {
|
||
flex: 1;
|
||
min-width: 0;
|
||
position: relative;
|
||
}
|
||
|
||
.ap-explore-autocomplete__dropdown {
|
||
background: var(--color-background);
|
||
border: var(--border-width-thin) solid var(--color-outline);
|
||
border-radius: var(--border-radius-small);
|
||
box-shadow: 0 4px 12px hsl(var(--tint-neutral) 10% / 0.15);
|
||
left: 0;
|
||
max-height: 320px;
|
||
overflow-y: auto;
|
||
position: absolute;
|
||
right: 0;
|
||
top: 100%;
|
||
z-index: 100;
|
||
}
|
||
|
||
.ap-explore-autocomplete__item {
|
||
align-items: center;
|
||
background: none;
|
||
border: none;
|
||
color: var(--color-on-background);
|
||
cursor: pointer;
|
||
display: flex;
|
||
font-family: inherit;
|
||
font-size: var(--font-size-s);
|
||
gap: var(--space-s);
|
||
padding: var(--space-s) var(--space-m);
|
||
text-align: left;
|
||
width: 100%;
|
||
}
|
||
|
||
.ap-explore-autocomplete__item:hover,
|
||
.ap-explore-autocomplete__item--highlighted {
|
||
background: var(--color-offset);
|
||
}
|
||
|
||
.ap-explore-autocomplete__domain {
|
||
flex-shrink: 0;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.ap-explore-autocomplete__meta {
|
||
color: var(--color-on-offset);
|
||
display: flex;
|
||
flex: 1;
|
||
gap: var(--space-xs);
|
||
min-width: 0;
|
||
}
|
||
|
||
.ap-explore-autocomplete__software {
|
||
background: color-mix(in srgb, var(--color-primary) 12%, transparent);
|
||
border-radius: var(--border-radius-small);
|
||
font-size: var(--font-size-xs);
|
||
padding: 1px 6px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.ap-explore-autocomplete__mau {
|
||
font-size: var(--font-size-xs);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.ap-explore-autocomplete__status {
|
||
flex-shrink: 0;
|
||
font-size: var(--font-size-s);
|
||
}
|
||
|
||
.ap-explore-autocomplete__checking {
|
||
opacity: 0.5;
|
||
}
|
||
|
||
/* ---------- Popular accounts autocomplete ---------- */
|
||
|
||
.ap-lookup-autocomplete {
|
||
flex: 1;
|
||
min-width: 0;
|
||
position: relative;
|
||
}
|
||
|
||
.ap-lookup-autocomplete__dropdown {
|
||
background: var(--color-background);
|
||
border: var(--border-width-thin) solid var(--color-outline);
|
||
border-radius: var(--border-radius-small);
|
||
box-shadow: 0 4px 12px hsl(var(--tint-neutral) 10% / 0.15);
|
||
left: 0;
|
||
max-height: 320px;
|
||
overflow-y: auto;
|
||
position: absolute;
|
||
right: 0;
|
||
top: 100%;
|
||
z-index: 100;
|
||
}
|
||
|
||
.ap-lookup-autocomplete__item {
|
||
align-items: center;
|
||
background: none;
|
||
border: none;
|
||
color: var(--color-on-background);
|
||
cursor: pointer;
|
||
display: flex;
|
||
font-family: inherit;
|
||
font-size: var(--font-size-s);
|
||
gap: var(--space-s);
|
||
padding: var(--space-s) var(--space-m);
|
||
text-align: left;
|
||
width: 100%;
|
||
}
|
||
|
||
.ap-lookup-autocomplete__item:hover,
|
||
.ap-lookup-autocomplete__item--highlighted {
|
||
background: var(--color-offset);
|
||
}
|
||
|
||
.ap-lookup-autocomplete__avatar {
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
height: 28px;
|
||
object-fit: cover;
|
||
width: 28px;
|
||
}
|
||
|
||
.ap-lookup-autocomplete__info {
|
||
display: flex;
|
||
flex: 1;
|
||
flex-direction: column;
|
||
min-width: 0;
|
||
}
|
||
|
||
.ap-lookup-autocomplete__name {
|
||
font-weight: 600;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.ap-lookup-autocomplete__handle {
|
||
color: var(--color-on-offset);
|
||
font-size: var(--font-size-xs);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.ap-lookup-autocomplete__followers {
|
||
color: var(--color-on-offset);
|
||
flex-shrink: 0;
|
||
font-size: var(--font-size-xs);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* ==========================================================================
|
||
Explore: Tabbed Design
|
||
========================================================================== */
|
||
|
||
/* Tab bar wrapper: enables position:relative for fade gradient overlay */
|
||
.ap-explore-tabs-container {
|
||
position: relative;
|
||
}
|
||
|
||
/* Tab bar with right-edge fade to indicate horizontal overflow */
|
||
.ap-explore-tabs-nav {
|
||
padding-right: var(--space-l);
|
||
position: relative;
|
||
}
|
||
|
||
.ap-explore-tabs-nav::after {
|
||
background: linear-gradient(to right, transparent, var(--color-background) 80%);
|
||
content: "";
|
||
height: 100%;
|
||
pointer-events: none;
|
||
position: absolute;
|
||
right: 0;
|
||
top: 0;
|
||
width: 40px;
|
||
}
|
||
|
||
/* Tab wrapper: holds tab button + reorder/close controls together */
|
||
.ap-tab-wrapper {
|
||
align-items: stretch;
|
||
display: inline-flex;
|
||
position: relative;
|
||
}
|
||
|
||
/* Show controls on hover or when the tab is active */
|
||
.ap-tab-controls {
|
||
align-items: center;
|
||
display: none;
|
||
gap: 1px;
|
||
}
|
||
|
||
.ap-tab-wrapper:hover .ap-tab-controls,
|
||
.ap-tab-wrapper:focus-within .ap-tab-controls {
|
||
display: flex;
|
||
}
|
||
|
||
/* Individual control buttons (↑ ↓ ×) */
|
||
.ap-tab-control {
|
||
background: none;
|
||
border: none;
|
||
color: var(--color-on-offset);
|
||
cursor: pointer;
|
||
font-size: var(--font-size-xs);
|
||
line-height: 1;
|
||
padding: 2px 4px;
|
||
}
|
||
|
||
.ap-tab-control:hover {
|
||
color: var(--color-on-background);
|
||
}
|
||
|
||
.ap-tab-control:disabled {
|
||
cursor: default;
|
||
opacity: 0.3;
|
||
}
|
||
|
||
.ap-tab-control--remove {
|
||
color: var(--color-on-offset);
|
||
font-size: var(--font-size-s);
|
||
}
|
||
|
||
.ap-tab-control--remove:hover {
|
||
color: var(--color-error);
|
||
}
|
||
|
||
/* Truncate long domain names in tab labels */
|
||
.ap-tab__label {
|
||
display: inline-block;
|
||
max-width: 150px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* Scope badges on instance tabs */
|
||
.ap-tab__badge {
|
||
border-radius: var(--border-radius-small);
|
||
font-size: 0.65em;
|
||
font-weight: 700;
|
||
letter-spacing: 0.02em;
|
||
margin-left: var(--space-xs);
|
||
padding: 1px 4px;
|
||
text-transform: uppercase;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.ap-tab__badge--local {
|
||
background: color-mix(in srgb, var(--color-primary) 15%, transparent);
|
||
color: var(--color-primary-on-background);
|
||
}
|
||
|
||
.ap-tab__badge--federated {
|
||
background: color-mix(in srgb, var(--color-purple45) 15%, transparent);
|
||
color: var(--color-purple45);
|
||
}
|
||
|
||
/* +# button for adding hashtag tabs */
|
||
.ap-tab--add {
|
||
font-family: monospace;
|
||
font-weight: 700;
|
||
letter-spacing: -0.05em;
|
||
}
|
||
|
||
/* Inline hashtag form that appears when +# is clicked */
|
||
.ap-tab-add-hashtag {
|
||
align-items: center;
|
||
display: inline-flex;
|
||
gap: var(--space-xs);
|
||
}
|
||
|
||
.ap-tab-hashtag-form {
|
||
align-items: center;
|
||
display: flex;
|
||
gap: var(--space-xs);
|
||
}
|
||
|
||
.ap-tab-hashtag-form__prefix {
|
||
color: var(--color-on-offset);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.ap-tab-hashtag-form__input {
|
||
border: var(--border-width-thin) solid var(--color-outline);
|
||
border-radius: var(--border-radius-small);
|
||
font-family: inherit;
|
||
font-size: var(--font-size-s);
|
||
padding: 2px var(--space-s);
|
||
width: 8em;
|
||
}
|
||
|
||
.ap-tab-hashtag-form__input:focus {
|
||
border-color: var(--color-primary);
|
||
outline: 2px solid var(--color-primary);
|
||
outline-offset: -1px;
|
||
}
|
||
|
||
.ap-tab-hashtag-form__btn {
|
||
background: var(--color-primary);
|
||
border: none;
|
||
border-radius: var(--border-radius-small);
|
||
color: var(--color-on-primary);
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
font-size: var(--font-size-s);
|
||
padding: 2px var(--space-s);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.ap-tab-hashtag-form__btn:hover {
|
||
opacity: 0.85;
|
||
}
|
||
|
||
/* "Pin as tab" button in search results area */
|
||
.ap-explore-pin-bar {
|
||
margin-bottom: var(--space-s);
|
||
}
|
||
|
||
.ap-explore-pin-btn {
|
||
background: none;
|
||
border: var(--border-width-thin) solid var(--color-primary-on-background);
|
||
border-radius: var(--border-radius-small);
|
||
color: var(--color-primary-on-background);
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
font-size: var(--font-size-s);
|
||
padding: var(--space-xs) var(--space-m);
|
||
}
|
||
|
||
.ap-explore-pin-btn:hover {
|
||
background: color-mix(in srgb, var(--color-primary) 10%, transparent);
|
||
}
|
||
|
||
.ap-explore-pin-btn:disabled {
|
||
cursor: default;
|
||
opacity: 0.6;
|
||
}
|
||
|
||
/* Hashtag form row inside the search form */
|
||
.ap-explore-form__hashtag-row {
|
||
align-items: center;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: var(--space-xs);
|
||
margin-top: var(--space-s);
|
||
}
|
||
|
||
.ap-explore-form__hashtag-label {
|
||
color: var(--color-on-offset);
|
||
font-size: var(--font-size-s);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.ap-explore-form__hashtag-prefix {
|
||
color: var(--color-on-offset);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.ap-explore-form__hashtag-hint {
|
||
color: var(--color-on-offset);
|
||
font-size: var(--font-size-xs);
|
||
flex-basis: 100%;
|
||
}
|
||
|
||
.ap-explore-form__input--hashtag {
|
||
max-width: 200px;
|
||
width: auto;
|
||
}
|
||
|
||
/* Tab panel containers */
|
||
.ap-explore-instance-panel,
|
||
.ap-explore-hashtag-panel {
|
||
min-height: 120px;
|
||
}
|
||
|
||
/* Loading state */
|
||
.ap-explore-tab-loading {
|
||
align-items: center;
|
||
color: var(--color-on-offset);
|
||
display: flex;
|
||
justify-content: center;
|
||
padding: var(--space-xl);
|
||
}
|
||
|
||
.ap-explore-tab-loading--more {
|
||
padding-block: var(--space-m);
|
||
}
|
||
|
||
.ap-explore-tab-loading__text {
|
||
font-size: var(--font-size-s);
|
||
}
|
||
|
||
/* Error state */
|
||
.ap-explore-tab-error {
|
||
align-items: center;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-s);
|
||
padding: var(--space-xl);
|
||
}
|
||
|
||
.ap-explore-tab-error__message {
|
||
color: var(--color-error);
|
||
font-size: var(--font-size-s);
|
||
margin: 0;
|
||
}
|
||
|
||
.ap-explore-tab-error__retry {
|
||
background: none;
|
||
border: var(--border-width-thin) solid var(--color-primary-on-background);
|
||
border-radius: var(--border-radius-small);
|
||
color: var(--color-primary-on-background);
|
||
cursor: pointer;
|
||
font-size: var(--font-size-s);
|
||
padding: var(--space-xs) var(--space-s);
|
||
}
|
||
|
||
.ap-explore-tab-error__retry:hover {
|
||
background: color-mix(in srgb, var(--color-primary) 10%, transparent);
|
||
}
|
||
|
||
/* Empty state */
|
||
.ap-explore-tab-empty {
|
||
color: var(--color-on-offset);
|
||
font-size: var(--font-size-s);
|
||
padding: var(--space-xl);
|
||
text-align: center;
|
||
}
|
||
|
||
/* Infinite scroll sentinel — zero height, invisible */
|
||
.ap-tab-sentinel {
|
||
height: 1px;
|
||
visibility: hidden;
|
||
}
|