diff --git a/.gitignore b/.gitignore index 3c3629e..bfb974a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ node_modules +.playwright-cli/ +.playwright-mcp/ diff --git a/CLAUDE.md b/CLAUDE.md index a4347d3..272dc98 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -618,6 +618,37 @@ curl -s "https://rmendes.net/nodeinfo/2.1" | jq . - `@_followback@tags.pub` does not send Follow activities back despite accepting ours - Both suggest tags.pub's outbound delivery is broken — zero inbound requests from `activitypub-bot` user-agent have been observed +## Form Handling Convention + +Two form patterns are used in this plugin. New forms should follow the appropriate pattern. + +### Pattern 1: Traditional POST (data mutation forms) + +Used for: compose, profile editor, migration alias, notification mark-read/clear. + +- Standard `
` +- CSRF via `` +- Server processes, then redirects (PRG pattern) +- Success/error feedback via Indiekit's notification banner system +- Uses Indiekit form macros (`input`, `textarea`, `button`) where available + +### Pattern 2: Alpine.js Fetch (in-page CRUD operations) + +Used for: moderation add/remove keyword/server, tab management, federation actions. + +- Alpine.js `@submit.prevent` or `@click` handlers +- CSRF via `X-CSRF-Token` header in `fetch()` call +- Inline error display with `x-show="error"` and `role="alert"` +- Optimistic UI with rollback on failure +- No page reload — DOM updates in place + +### Rules + +- Do NOT mix patterns on the same page (one pattern per form) +- All forms MUST include CSRF protection (hidden field OR header) +- Error feedback: Pattern 1 uses redirect + banner, Pattern 2 uses inline `x-show="error"` +- Success feedback: Pattern 1 uses redirect + banner, Pattern 2 uses inline DOM update or element removal + ## CSS Conventions The reader CSS (`assets/reader.css`) uses Indiekit's theme custom properties for automatic dark mode support: diff --git a/assets/css/base.css b/assets/css/base.css new file mode 100644 index 0000000..c2d598e --- /dev/null +++ b/assets/css/base.css @@ -0,0 +1,144 @@ +/** + * ActivityPub Reader Styles + * Card-based layout inspired by Phanpy/Elk + * Uses Indiekit CSS custom properties for automatic dark mode support + */ + +/* ========================================================================== + Breadcrumb Navigation + ========================================================================== */ + +.ap-breadcrumb { + display: flex; + align-items: center; + gap: var(--space-xs); + margin-bottom: var(--space-m); + font-size: var(--font-size-s); + color: var(--color-on-offset); +} + +.ap-breadcrumb a { + color: var(--color-primary-on-background); + text-decoration: none; +} + +.ap-breadcrumb a:hover { + text-decoration: underline; +} + +.ap-breadcrumb__separator { + color: var(--color-on-offset); +} + +.ap-breadcrumb__current { + color: var(--color-on-background); + font-weight: 600; +} + +/* ========================================================================== + Fediverse Lookup + ========================================================================== */ + +.ap-lookup { + display: flex; + gap: var(--space-xs); + margin-bottom: var(--space-m); +} + +.ap-lookup__input { + border: var(--border-width-thin) solid var(--color-outline); + border-radius: var(--border-radius-small); + background: var(--color-offset); + box-sizing: border-box; + color: var(--color-on-background); + font-family: inherit; + font-size: var(--font-size-m); + padding: var(--space-s) var(--space-m); + width: 100%; +} + +.ap-lookup__input::placeholder { + color: var(--color-on-offset); +} + +.ap-lookup__input:focus { + outline: 2px solid var(--color-primary); + outline-offset: -1px; + border-color: var(--color-primary); +} + +.ap-lookup__btn { + padding: var(--space-s) var(--space-m); + border: var(--border-width-thin) solid var(--color-primary); + border-radius: var(--border-radius-small); + background: var(--color-primary); + color: var(--color-on-primary); + font-size: var(--font-size-m); + font-family: inherit; + font-weight: 600; + cursor: pointer; + white-space: nowrap; +} + +.ap-lookup__btn:hover { + opacity: 0.9; +} + +/* ========================================================================== + Tab Navigation + ========================================================================== */ + +.ap-tabs { + border-bottom: var(--border-width-thin) solid var(--color-outline); + display: flex; + gap: var(--space-xs); + margin-bottom: var(--space-m); + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +.ap-tab { + border-bottom: var(--border-width-thick) solid transparent; + color: var(--color-on-offset); + font-size: var(--font-size-m); + padding: var(--space-s) var(--space-m); + text-decoration: none; + transition: + color 0.2s ease, + border-color 0.2s ease; + white-space: nowrap; +} + +.ap-tab:hover { + color: var(--color-on-background); +} + +.ap-tab--active { + border-bottom-color: var(--color-primary); + color: var(--color-primary-on-background); + font-weight: 600; +} + +.ap-tab__count { + background: var(--color-offset-variant); + border-radius: var(--border-radius-large); + font-size: var(--font-size-xs); + font-weight: 600; + margin-left: var(--space-xs); + padding: 1px 6px; +} + +.ap-tab--active .ap-tab__count { + background: var(--color-primary); + color: var(--color-on-primary, var(--color-neutral99)); +} + +/* ========================================================================== + Timeline Layout + ========================================================================== */ + +.ap-timeline { + display: flex; + flex-direction: column; + gap: var(--space-m); +} diff --git a/assets/css/card.css b/assets/css/card.css new file mode 100644 index 0000000..88e1f9c --- /dev/null +++ b/assets/css/card.css @@ -0,0 +1,377 @@ +/* ========================================================================== + Item Card — Base + ========================================================================== */ + +.ap-card { + background: var(--color-offset); + border: var(--border-width-thin) solid var(--color-outline); + border-left: 3px solid var(--color-outline); + border-radius: var(--border-radius-small); + overflow: hidden; + padding: var(--space-m); + box-shadow: 0 1px 2px hsl(var(--tint-neutral) 10% / 0.04); + transition: + box-shadow 0.2s ease, + border-color 0.2s ease; +} + +.ap-card:hover { + border-color: var(--color-outline-variant); + border-left-color: var(--color-outline-variant); + box-shadow: 0 2px 8px hsl(var(--tint-neutral) 10% / 0.08); +} + +/* ========================================================================== + Item Card — Post Type Differentiation + ========================================================================== */ + +/* Notes: default purple-ish accent (the most common type) */ +.ap-card--note { + border-left-color: var(--color-purple45); +} + +.ap-card--note:hover { + border-left-color: var(--color-purple45); +} + +/* Articles: green accent (long-form content stands out) */ +.ap-card--article { + border-left-color: var(--color-green50); +} + +.ap-card--article:hover { + border-left-color: var(--color-green50); +} + +/* Boosts: yellow accent (shared content) */ +.ap-card--boost { + border-left-color: var(--color-yellow50); +} + +.ap-card--boost:hover { + border-left-color: var(--color-yellow50); +} + +/* Replies: blue accent (via primary color) */ +.ap-card--reply { + border-left-color: var(--color-primary); +} + +.ap-card--reply:hover { + border-left-color: var(--color-primary); +} + +/* ========================================================================== + Boost Header + ========================================================================== */ + +.ap-card__boost { + color: var(--color-on-offset); + font-size: var(--font-size-s); + margin-bottom: var(--space-s); + padding-bottom: var(--space-xs); +} + +.ap-card__boost a { + color: var(--color-on-offset); + font-weight: 600; + text-decoration: none; +} + +.ap-card__boost a:hover { + color: var(--color-on-background); + text-decoration: underline; +} + +/* ========================================================================== + Reply Context + ========================================================================== */ + +.ap-card__reply-to { + color: var(--color-on-offset); + font-size: var(--font-size-s); + margin-bottom: var(--space-s); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ap-card__reply-to a { + color: var(--color-primary-on-background); + text-decoration: none; +} + +.ap-card__reply-to a:hover { + text-decoration: underline; +} + +/* ========================================================================== + Author Header + ========================================================================== */ + +.ap-card__author { + align-items: center; + display: flex; + gap: var(--space-s); + margin-bottom: var(--space-s); +} + +.ap-card__avatar-wrap { + flex-shrink: 0; + height: 44px; + position: relative; + width: 44px; +} + +.ap-card__avatar { + border: var(--border-width-thin) solid var(--color-outline); + border-radius: 50%; + height: 44px; + object-fit: cover; + width: 44px; +} + +.ap-card__avatar-wrap > img { + position: absolute; + inset: 0; + z-index: 1; +} + +.ap-card__avatar--default { + align-items: center; + background: var(--color-offset-variant); + color: var(--color-on-offset); + display: inline-flex; + font-size: 1.1em; + font-weight: 600; + justify-content: center; +} + +.ap-card__author-info { + display: flex; + flex-direction: column; + flex: 1; + gap: 1px; + min-width: 0; +} + +.ap-card__author-name { + font-size: 0.95em; + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ap-card__author-name a { + color: inherit; + text-decoration: none; +} + +.ap-card__author-name a:hover { + text-decoration: underline; +} + +.ap-card__bot-badge { + display: inline-block; + font-size: 0.6rem; + font-weight: 700; + line-height: 1; + padding: 0.15em 0.35em; + margin-left: 0.3em; + border: var(--border-width-thin) solid var(--color-on-offset); + border-radius: var(--border-radius-small); + color: var(--color-on-offset); + vertical-align: middle; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.ap-card__author-handle { + color: var(--color-on-offset); + font-size: var(--font-size-s); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ap-card__timestamp { + color: var(--color-on-offset); + flex-shrink: 0; + font-size: var(--font-size-s); +} + +.ap-card__edited { + font-size: var(--font-size-xs); + margin-left: 0.2em; +} + +.ap-card__visibility { + font-size: var(--font-size-xs); + margin-left: 0.3em; + opacity: 0.7; +} + +.ap-card__timestamp-link { + color: inherit; + text-decoration: none; + display: flex; + align-items: center; + gap: 0; +} + +.ap-card__timestamp-link:hover { + text-decoration: underline; + color: var(--color-primary-on-background); +} + +/* ========================================================================== + Post Title (Articles) + ========================================================================== */ + +.ap-card__title { + font-size: var(--font-size-l); + font-weight: 600; + line-height: var(--line-height-tight); + margin-bottom: var(--space-s); +} + +.ap-card__title a { + color: inherit; + text-decoration: none; +} + +.ap-card__title a:hover { + text-decoration: underline; +} + +/* ========================================================================== + Content + ========================================================================== */ + +.ap-card__content { + color: var(--color-on-background); + line-height: calc(4 / 3 * 1em); + margin-bottom: var(--space-s); + overflow-wrap: break-word; + word-break: break-word; +} + +.ap-card__content a { + color: var(--color-primary-on-background); +} + +.ap-card__content p { + margin-bottom: var(--space-xs); +} + +.ap-card__content p:last-child { + margin-bottom: 0; +} + +.ap-card__content blockquote { + border-left: var(--border-width-thickest) solid var(--color-outline); + margin: var(--space-s) 0; + padding-left: var(--space-m); +} + +.ap-card__content pre { + background: var(--color-offset-variant); + border-radius: var(--border-radius-small); + overflow-x: auto; + padding: var(--space-s); +} + +.ap-card__content code { + background: var(--color-offset-variant); + border-radius: var(--border-radius-small); + font-size: 0.9em; + padding: 1px 4px; +} + +.ap-card__content pre code { + background: none; + padding: 0; +} + +.ap-card__content img { + border-radius: var(--border-radius-small); + height: auto; + max-width: 100%; +} + +/* @mentions — keep inline, style as subtle links */ +.ap-card__content .h-card { + display: inline; +} + +.ap-card__content .h-card a, +.ap-card__content a.u-url.mention { + display: inline; + color: var(--color-on-offset); + text-decoration: none; + white-space: nowrap; +} + +.ap-card__content .h-card a span, +.ap-card__content a.u-url.mention span { + display: inline; +} + +.ap-card__content .h-card a:hover, +.ap-card__content a.u-url.mention:hover { + color: var(--color-primary-on-background); + text-decoration: underline; +} + +/* Hashtag mentions — keep inline, subtle styling */ +.ap-card__content a.mention.hashtag { + display: inline; + color: var(--color-on-offset); + text-decoration: none; + white-space: nowrap; +} + +.ap-card__content a.mention.hashtag span { + display: inline; +} + +.ap-card__content a.mention.hashtag:hover { + color: var(--color-primary-on-background); + text-decoration: underline; +} + +/* Mastodon's invisible/ellipsis spans for long URLs */ +.ap-card__content .invisible { + display: none; +} + +.ap-card__content .ellipsis::after { + content: "…"; +} + +/* ========================================================================== + Content Warning + ========================================================================== */ + +.ap-card__cw { + margin-bottom: var(--space-s); +} + +.ap-card__cw-toggle { + background: var(--color-offset-variant); + border: var(--border-width-thin) solid var(--color-outline); + border-radius: var(--border-radius-small); + color: var(--color-on-background); + cursor: pointer; + display: block; + font-size: var(--font-size-s); + padding: var(--space-s) var(--space-m); + text-align: left; + transition: background 0.2s ease; + width: 100%; +} + +.ap-card__cw-toggle:hover { + background: var(--color-offset-variant-darker); +} diff --git a/assets/css/compose.css b/assets/css/compose.css new file mode 100644 index 0000000..6fb0be9 --- /dev/null +++ b/assets/css/compose.css @@ -0,0 +1,169 @@ +/* ========================================================================== + Compose Form + ========================================================================== */ + +.ap-compose__context { + background: var(--color-offset); + border-left: var(--border-width-thickest) solid var(--color-primary); + border-radius: var(--border-radius-small); + margin-bottom: var(--space-m); + padding: var(--space-m); +} + +.ap-compose__context-label { + color: var(--color-on-offset); + font-size: var(--font-size-s); + margin-bottom: var(--space-xs); +} + +.ap-compose__context-author a { + font-weight: 600; + text-decoration: none; +} + +.ap-compose__context-text { + border: 0; + font-size: var(--font-size-s); + line-height: var(--line-height-loose); + margin: var(--space-xs) 0; + padding: 0; +} + +.ap-compose__context-link { + color: var(--color-on-offset); + font-size: var(--font-size-s); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ap-compose__form { + display: flex; + flex-direction: column; + gap: var(--space-m); +} + +.ap-compose__editor { + position: relative; +} + +.ap-compose__textarea { + background: var(--color-background); + border: var(--border-width-thick) solid var(--color-outline); + border-radius: var(--border-radius-small); + color: var(--color-on-background); + font-family: inherit; + font-size: var(--font-size-m); + line-height: var(--line-height-prose); + padding: var(--space-s); + resize: vertical; + width: 100%; +} + +.ap-compose__textarea:focus { + border-color: var(--color-primary); + outline: var(--border-width-thick) solid var(--color-primary); + outline-offset: -2px; +} + +.ap-compose__cw { + display: flex; + flex-direction: column; + gap: var(--space-xs); +} + +.ap-compose__cw-toggle { + cursor: pointer; + display: flex; + align-items: center; + gap: var(--space-xs); + font-size: var(--font-size-s); + color: var(--color-on-offset); +} + +.ap-compose__cw-input { + border: var(--border-width-thin) solid var(--color-outline); + border-radius: var(--border-radius-small); + background: var(--color-offset); + color: var(--color-on-background); + font: inherit; + font-size: var(--font-size-s); + padding: var(--space-s); + width: 100%; +} + +.ap-compose__cw-input:focus { + border-color: var(--color-primary); + outline: none; +} + +.ap-compose__visibility { + border: var(--border-width-thin) solid var(--color-outline); + border-radius: var(--border-radius-small); + display: flex; + flex-wrap: wrap; + gap: var(--space-s) var(--space-m); + padding: var(--space-m); +} + +.ap-compose__visibility legend { + font-weight: 600; +} + +.ap-compose__visibility-option { + cursor: pointer; + display: flex; + align-items: center; + gap: var(--space-xs); + font-size: var(--font-size-s); +} + +.ap-compose__syndication { + border: var(--border-width-thin) solid var(--color-outline); + border-radius: var(--border-radius-small); + display: flex; + flex-direction: column; + gap: var(--space-xs); + padding: var(--space-m); +} + +.ap-compose__syndication legend { + font-weight: 600; +} + +.ap-compose__syndication-target { + cursor: pointer; + display: flex; + gap: var(--space-xs); +} + +.ap-compose__actions { + align-items: center; + display: flex; + gap: var(--space-m); +} + +.ap-compose__submit { + background: var(--color-primary); + border: 0; + border-radius: var(--border-radius-small); + color: var(--color-on-primary, var(--color-neutral99)); + cursor: pointer; + font-size: var(--font-size-m); + font-weight: 600; + padding: var(--space-s) var(--space-l); +} + +.ap-compose__submit:hover { + opacity: 0.9; +} + +.ap-compose__cancel { + color: var(--color-on-offset); + text-decoration: none; +} + +.ap-compose__cancel:hover { + color: var(--color-on-background); + text-decoration: underline; +} diff --git a/assets/css/dark-mode.css b/assets/css/dark-mode.css new file mode 100644 index 0000000..4f405b8 --- /dev/null +++ b/assets/css/dark-mode.css @@ -0,0 +1,94 @@ +/* ========================================================================== + Dark Mode Overrides + Softens saturated colors that are uncomfortable on dark backgrounds. + Uses Indiekit's existing light-variant tokens (red80, green90, yellow90) + which are designed for dark surfaces. + ========================================================================== */ + +@media (prefers-color-scheme: dark) { + + /* --- Action button hover states: softer colors, more visible tinted backgrounds --- */ + .ap-card__action--reply:hover { + background: color-mix(in srgb, var(--color-primary) 18%, transparent); + color: var(--color-primary-on-background); + } + + .ap-card__action--boost:hover { + background: color-mix(in srgb, var(--color-green50) 18%, transparent); + color: var(--color-green90); + } + + .ap-card__action--like:hover { + background: color-mix(in srgb, var(--color-red45) 18%, transparent); + color: var(--color-red80); + } + + .ap-card__action--save:hover { + background: color-mix(in srgb, var(--color-primary) 18%, transparent); + color: var(--color-primary-on-background); + } + + /* --- Active interaction states --- */ + .ap-card__action--like.ap-card__action--active { + background: color-mix(in srgb, var(--color-red45) 18%, transparent); + color: var(--color-red80); + } + + .ap-card__action--boost.ap-card__action--active { + background: color-mix(in srgb, var(--color-green50) 18%, transparent); + color: var(--color-green90); + } + + .ap-card__action--save.ap-card__action--active { + background: color-mix(in srgb, var(--color-primary) 18%, transparent); + color: var(--color-primary-on-background); + } + + /* --- Post-type left border accents: desaturated for dark surfaces --- */ + .ap-card--note, + .ap-card--note:hover { + border-left-color: var(--color-purple90); + } + + .ap-card--article, + .ap-card--article:hover { + border-left-color: var(--color-green90); + } + + .ap-card--boost, + .ap-card--boost:hover { + border-left-color: var(--color-yellow90); + } + + .ap-card--reply, + .ap-card--reply:hover { + border-left-color: var(--color-primary-on-background); + } + + /* --- Notification unread glow: toned down --- */ + .ap-notification--unread { + border-color: var(--color-yellow90); + box-shadow: 0 0 6px 0 color-mix(in srgb, var(--color-yellow50) 15%, transparent); + } + + /* --- Post detail highlight ring: softened --- */ + .ap-post-detail__main .ap-card { + border-color: color-mix(in srgb, var(--color-primary) 50%, transparent); + box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-primary) 50%, transparent); + } + + /* --- Card shadows: use light tint instead of black --- */ + .ap-card { + box-shadow: 0 1px 2px hsl(var(--tint-neutral) 90% / 0.04); + } + + .ap-card:hover { + box-shadow: 0 2px 8px hsl(var(--tint-neutral) 90% / 0.06); + } + + /* --- Tab badge federated: soften purple --- */ + .ap-tab__badge--federated { + color: var(--color-purple90); + background: color-mix(in srgb, var(--color-purple45) 18%, transparent); + } +} diff --git a/assets/css/explore.css b/assets/css/explore.css new file mode 100644 index 0000000..e849965 --- /dev/null +++ b/assets/css/explore.css @@ -0,0 +1,530 @@ +/* ========================================================================== + 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; +} diff --git a/assets/css/features.css b/assets/css/features.css new file mode 100644 index 0000000..715f43f --- /dev/null +++ b/assets/css/features.css @@ -0,0 +1,436 @@ +/* ========================================================================== + Post Detail View — Thread Layout + ========================================================================== */ + +.ap-post-detail__back { + margin-bottom: var(--space-m); +} + +.ap-post-detail__back-link { + color: var(--color-primary-on-background); + font-size: var(--font-size-s); + text-decoration: none; +} + +.ap-post-detail__back-link:hover { + text-decoration: underline; +} + +.ap-post-detail__not-found { + background: var(--color-offset); + border-radius: var(--border-radius-small); + color: var(--color-on-offset); + padding: var(--space-l); + text-align: center; +} + +.ap-post-detail__section-title { + color: var(--color-on-offset); + font-size: var(--font-size-s); + font-weight: 600; + margin: var(--space-m) 0 var(--space-s); + padding-bottom: var(--space-xs); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +/* Parent posts — indented with left border to show thread chain */ +.ap-post-detail__parents { + border-left: 3px solid var(--color-outline); + margin-bottom: var(--space-s); + padding-left: var(--space-m); +} + +.ap-post-detail__parent-item .ap-card { + opacity: 0.85; +} + +/* Main post — highlighted */ +.ap-post-detail__main { + margin-bottom: var(--space-m); +} + +.ap-post-detail__main .ap-card { + border-color: var(--color-primary); + box-shadow: 0 0 0 1px var(--color-primary); +} + +/* Replies — indented from the other side */ +.ap-post-detail__replies { + margin-left: var(--space-l); +} + +.ap-post-detail__reply-item { + border-left: 2px solid var(--color-outline); + padding-left: var(--space-m); + margin-bottom: var(--space-xs); +} + +/* ========================================================================== + Tag Timeline Header + ========================================================================== */ + +.ap-tag-header { + align-items: flex-start; + background: var(--color-offset); + border-bottom: var(--border-width-thin) solid var(--color-outline); + border-radius: var(--border-radius-small); + display: flex; + gap: var(--space-m); + justify-content: space-between; + margin-bottom: var(--space-m); + padding: var(--space-m); +} + +.ap-tag-header__title { + font-size: var(--font-size-xl); + font-weight: 600; + margin: 0 0 var(--space-xs); +} + +.ap-tag-header__count { + color: var(--color-on-offset); + font-size: var(--font-size-s); + margin: 0; +} + +.ap-tag-header__actions { + align-items: center; + display: flex; + flex-shrink: 0; + gap: var(--space-s); +} + +.ap-tag-header__follow-btn { + background: var(--color-primary); + border: none; + border-radius: var(--border-radius-small); + color: var(--color-on-primary, var(--color-neutral99)); + cursor: pointer; + font-size: var(--font-size-s); + padding: var(--space-xs) var(--space-s); +} + +.ap-tag-header__follow-btn:hover { + opacity: 0.85; +} + +.ap-tag-header__unfollow-btn { + background: transparent; + border: var(--border-width-thin) solid var(--color-outline); + border-radius: var(--border-radius-small); + color: var(--color-on-background); + cursor: pointer; + font-size: var(--font-size-s); + padding: var(--space-xs) var(--space-s); +} + +.ap-tag-header__unfollow-btn:hover { + border-color: var(--color-on-background); +} + +.ap-tag-header__back { + color: var(--color-on-offset); + font-size: var(--font-size-s); + text-decoration: none; +} + +.ap-tag-header__back:hover { + color: var(--color-on-background); + text-decoration: underline; +} + +@media (max-width: 640px) { + .ap-tag-header { + flex-direction: column; + gap: var(--space-s); + } + + .ap-tag-header__actions { + flex-wrap: wrap; + } +} + +/* ========================================================================== + Reader Tools Bar (Explore link, etc.) + ========================================================================== */ + +.ap-reader-tools { + display: flex; + gap: var(--space-s); + justify-content: flex-end; + margin-bottom: var(--space-s); +} + +.ap-reader-tools__explore { + color: var(--color-on-offset); + font-size: var(--font-size-s); + text-decoration: none; +} + +.ap-reader-tools__explore:hover { + color: var(--color-on-background); + text-decoration: underline; +} + +/* Followed tags bar */ +.ap-followed-tags { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--space-xs); + padding: var(--space-xs) 0; + margin-bottom: var(--space-s); + font-size: var(--font-size-s); +} + +.ap-followed-tags__label { + color: var(--color-on-offset); + font-weight: 600; +} + +/* ========================================================================== + New Posts Banner + ========================================================================== */ + +.ap-new-posts-banner { + left: 0; + position: sticky; + right: 0; + top: 0; + z-index: 10; +} + +.ap-new-posts-banner__btn { + background: var(--color-primary); + border: none; + border-radius: var(--border-radius-small); + color: var(--color-on-primary); + cursor: pointer; + display: block; + font-family: inherit; + font-size: var(--font-size-s); + margin: 0 auto var(--space-s); + padding: var(--space-xs) var(--space-m); + text-align: center; + width: auto; +} + +.ap-new-posts-banner__btn:hover { + opacity: 0.9; +} + +/* ========================================================================== + Read State + ========================================================================== */ + +.ap-card--read { + opacity: 0.7; + transition: opacity 0.3s ease; +} + +.ap-card--read:hover { + opacity: 1; +} + +/* ========================================================================== + Unread Toggle + ========================================================================== */ + +.ap-unread-toggle { + margin-left: auto; +} + +.ap-unread-toggle--active { + background: color-mix(in srgb, var(--color-primary) 12%, transparent); + font-weight: 600; +} + +/* ========================================================================== + Quote Embeds + ========================================================================== */ + +.ap-quote-embed { + border: var(--border-width-thin) solid var(--color-outline); + border-radius: var(--border-radius-small); + margin-top: var(--space-s); + overflow: hidden; + transition: border-color 0.15s ease; +} + +.ap-quote-embed:hover { + border-color: var(--color-outline-variant); +} + +.ap-quote-embed--pending { + border-style: dashed; +} + +.ap-quote-embed__link { + color: inherit; + display: block; + padding: var(--space-s) var(--space-m); + text-decoration: none; +} + +.ap-quote-embed__link:hover { + background: color-mix(in srgb, var(--color-offset) 50%, transparent); +} + +.ap-quote-embed__author { + align-items: center; + display: flex; + gap: var(--space-xs); + margin-bottom: var(--space-xs); +} + +.ap-quote-embed__avatar { + border-radius: 50%; + flex-shrink: 0; + height: 24px; + object-fit: cover; + width: 24px; +} + +.ap-quote-embed__avatar--default { + align-items: center; + background: var(--color-offset); + color: var(--color-on-offset); + display: inline-flex; + font-size: var(--font-size-xs); + font-weight: 600; + justify-content: center; +} + +.ap-quote-embed__author-info { + flex: 1; + min-width: 0; +} + +.ap-quote-embed__name { + font-size: var(--font-size-s); + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ap-quote-embed__handle { + color: var(--color-on-offset); + font-size: var(--font-size-xs); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ap-quote-embed__time { + color: var(--color-on-offset); + flex-shrink: 0; + font-size: var(--font-size-xs); + white-space: nowrap; +} + +.ap-quote-embed__title { + font-size: var(--font-size-s); + font-weight: 600; + margin: 0 0 var(--space-xs); +} + +.ap-quote-embed__content { + color: var(--color-on-background); + font-size: var(--font-size-s); + line-height: calc(4 / 3 * 1em); + max-height: calc(1.333em * 6); + overflow: hidden; +} + +.ap-quote-embed__content a { + display: inline; +} + +.ap-quote-embed__content a span { + display: inline; +} + +.ap-quote-embed__content p { + margin: 0 0 var(--space-xs); +} + +.ap-quote-embed__content p:last-child { + margin-bottom: 0; +} + +.ap-quote-embed__media { + margin-top: var(--space-xs); +} + +.ap-quote-embed__photo { + border-radius: var(--border-radius-small); + max-height: 160px; + max-width: 100%; + object-fit: cover; +} + +/* ========================================================================== + Poll / Question + ========================================================================== */ + +.ap-poll { + margin-top: var(--space-s); +} + +.ap-poll__option { + position: relative; + padding: var(--space-xs) var(--space-s); + margin-bottom: var(--space-xs); + border-radius: var(--border-radius-small); + background: var(--color-offset); + overflow: hidden; +} + +.ap-poll__bar { + position: absolute; + top: 0; + left: 0; + bottom: 0; + background: var(--color-primary); + opacity: 0.15; + border-radius: var(--border-radius-small); +} + +.ap-poll__label { + position: relative; + font-size: var(--font-size-s); + color: var(--color-on-background); +} + +.ap-poll__votes { + position: relative; + float: right; + font-size: var(--font-size-s); + font-weight: 600; + color: var(--color-on-offset); +} + +.ap-poll__footer { + font-size: var(--font-size-xs); + color: var(--color-on-offset); + margin-top: var(--space-xs); +} + +/* Hashtag tab sources info line */ +.ap-hashtag-sources { + color: var(--color-on-offset); + font-size: var(--font-size-s); + margin: 0; + padding: var(--space-s) 0 var(--space-xs); +} + +/* Custom emoji */ +.ap-custom-emoji { + height: 1.2em; + width: auto; + vertical-align: middle; + display: inline; + margin: 0 0.05em; +} diff --git a/assets/css/federation.css b/assets/css/federation.css new file mode 100644 index 0000000..9fab396 --- /dev/null +++ b/assets/css/federation.css @@ -0,0 +1,242 @@ +/* ========================================================================== + Federation Management + ========================================================================== */ + +.ap-federation__section { + margin-block-end: var(--space-l); +} + +.ap-federation__section h2 { + margin-block-end: var(--space-s); +} + +.ap-federation__stats-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr)); + gap: var(--space-s); +} + +.ap-federation__stat-card { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-xs); + padding: var(--space-s); + background: var(--color-offset); + border-radius: var(--border-radius-small); + text-align: center; +} + +.ap-federation__stat-count { + font-size: var(--font-size-xl); + font-weight: 600; + color: var(--color-on-background); +} + +.ap-federation__stat-label { + font-size: var(--font-size-s); + color: var(--color-on-offset); + word-break: break-word; +} + +.ap-federation__actions-row { + display: flex; + flex-wrap: wrap; + gap: var(--space-s); + align-items: center; +} + +.ap-federation__result { + margin-block-start: var(--space-xs); + color: var(--color-green50); + font-size: var(--font-size-s); +} + +.ap-federation__error { + margin-block-start: var(--space-xs); + color: var(--color-red45); + font-size: var(--font-size-s); +} + +.ap-federation__lookup-form { + display: flex; + gap: var(--space-s); +} + +.ap-federation__lookup-input { + flex: 1; + min-width: 0; + padding: 0.5rem 0.75rem; + border: var(--border-width-thin) solid var(--color-outline); + border-radius: var(--border-radius-small); + font: inherit; + color: var(--color-on-background); + background: var(--color-background); +} + +.ap-federation__json-view { + margin-block-start: var(--space-s); + padding: var(--space-m); + background: var(--color-offset); + border-radius: var(--border-radius-small); + font-family: monospace; + font-size: var(--font-size-s); + color: var(--color-on-background); + max-height: 24rem; + overflow: auto; + white-space: pre-wrap; + word-break: break-word; +} + +.ap-federation__posts-list { + display: flex; + flex-direction: column; + gap: var(--space-xs); +} + +.ap-federation__post-row { + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--space-m); + padding: var(--space-s); + background: var(--color-offset); + border-radius: var(--border-radius-small); +} + +.ap-federation__post-info { + display: flex; + flex-direction: column; + gap: var(--space-xs); + min-width: 0; +} + +.ap-federation__post-title { + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.ap-federation__post-meta { + display: flex; + align-items: center; + gap: var(--space-xs); + font-size: var(--font-size-s); + color: var(--color-on-offset); +} + +.ap-federation__post-actions { + display: flex; + gap: var(--space-xs); + flex-shrink: 0; +} + +.ap-federation__post-btn { + padding: var(--space-xs) var(--space-s); + font-size: var(--font-size-s); + border: var(--border-width-thin) solid var(--color-outline); + border-radius: var(--border-radius-small); + background: var(--color-background); + color: var(--color-on-background); + cursor: pointer; +} + +.ap-federation__post-btn:hover { + background: var(--color-offset); +} + +.ap-federation__post-btn--danger { + color: var(--color-red45); + border-color: var(--color-red45); +} + +.ap-federation__post-btn--danger:hover { + background: color-mix(in srgb, var(--color-red45) 10%, transparent); +} + +.ap-federation__modal-overlay { + position: fixed; + inset: 0; + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + background: hsl(var(--tint-neutral) 10% / 0.5); +} + +.ap-federation__modal { + width: min(90vw, 48rem); + max-height: 80vh; + display: flex; + flex-direction: column; + background: var(--color-background); + border-radius: var(--border-radius-small); + box-shadow: 0 4px 24px hsl(var(--tint-neutral) 10% / 0.2); +} + +.ap-federation__modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--space-s) var(--space-m); + border-block-end: var(--border-width-thin) solid var(--color-outline); +} + +.ap-federation__modal-header h3 { + margin: 0; + font-size: var(--font-size-m); +} + +.ap-federation__modal-close { + font-size: var(--font-size-xl); + line-height: 1; + padding: 0 var(--space-xs); + border: none; + background: none; + color: var(--color-on-offset); + cursor: pointer; +} + +.ap-federation__modal .ap-federation__json-view { + margin: 0; + border-radius: 0 0 var(--border-radius-small) var(--border-radius-small); + flex: 1; + overflow: auto; +} + +@media (max-width: 40rem) { + .ap-federation__post-row { + flex-direction: column; + align-items: flex-start; + } + + .ap-federation__lookup-form { + flex-direction: column; + } +} + +/* Follow request approve/reject actions */ +.ap-follow-request { + margin-block-end: var(--space-m); +} + +.ap-follow-request__actions { + display: flex; + gap: var(--space-s); + margin-block-start: var(--space-xs); + padding-inline-start: var(--space-l); +} + +.ap-follow-request__form { + display: inline; +} + +.button--danger { + background-color: var(--color-red45); + color: white; +} + +.button--danger:hover { + background-color: var(--color-red35, #c0392b); +} diff --git a/assets/css/interactions.css b/assets/css/interactions.css new file mode 100644 index 0000000..d04313e --- /dev/null +++ b/assets/css/interactions.css @@ -0,0 +1,236 @@ +/* ========================================================================== + Tags + ========================================================================== */ + +.ap-card__tags { + display: flex; + flex-wrap: wrap; + gap: var(--space-xs); + margin-bottom: var(--space-s); +} + +.ap-card__tag { + background: var(--color-offset-variant); + border-radius: var(--border-radius-large); + color: var(--color-on-offset); + font-size: var(--font-size-s); + padding: 2px var(--space-xs); + text-decoration: none; +} + +.ap-card__tag:hover { + background: var(--color-offset-variant-darker); + color: var(--color-on-background); +} + +.ap-card__mention { + background: color-mix(in srgb, var(--color-primary) 12%, transparent); + border-radius: var(--border-radius-large); + color: var(--color-primary-on-background); + font-size: var(--font-size-s); + padding: 2px var(--space-xs); + text-decoration: none; +} + +.ap-card__mention:hover { + background: color-mix(in srgb, var(--color-primary) 22%, transparent); + color: var(--color-primary-on-background); +} + +.ap-card__mention--legacy { + cursor: default; + opacity: 0.7; +} + +/* Hashtag stuffing collapse */ +.ap-hashtag-overflow { + margin: var(--space-xs) 0; + font-size: var(--font-size-s); +} + +.ap-hashtag-overflow summary { + cursor: pointer; + color: var(--color-on-offset); + list-style: none; +} + +.ap-hashtag-overflow summary::before { + content: "▸ "; +} + +.ap-hashtag-overflow[open] summary::before { + content: "▾ "; +} + +.ap-hashtag-overflow p { + margin-top: var(--space-xs); +} + +/* ========================================================================== + Interaction Buttons + ========================================================================== */ + +.ap-card__actions { + border-top: var(--border-width-thin) solid var(--color-outline); + display: flex; + flex-wrap: wrap; + gap: 2px; + padding-top: var(--space-s); +} + +.ap-card__action { + align-items: center; + background: transparent; + border: 0; + border-radius: var(--border-radius-small); + color: var(--color-on-offset); + cursor: pointer; + display: inline-flex; + font-size: var(--font-size-s); + gap: 0.3em; + min-height: 36px; + padding: 0.25em 0.6em; + text-decoration: none; + transition: + background-color 0.15s ease, + color 0.15s ease; +} + +.ap-card__action:hover { + background: var(--color-offset-variant); + color: var(--color-on-background); +} + +/* Color-coded hover states per action type */ +.ap-card__action--reply:hover { + background: color-mix(in srgb, var(--color-primary) 12%, transparent); + color: var(--color-primary); +} + +.ap-card__action--boost:hover { + background: color-mix(in srgb, var(--color-green50) 12%, transparent); + color: var(--color-green50); +} + +.ap-card__action--like:hover { + background: color-mix(in srgb, var(--color-red45) 12%, transparent); + color: var(--color-red45); +} + +.ap-card__action--link:hover { + background: var(--color-offset-variant); + color: var(--color-on-background); +} + +.ap-card__action--save:hover { + background: color-mix(in srgb, var(--color-primary) 12%, transparent); + color: var(--color-primary); +} + +/* Active interaction states */ +.ap-card__action--like.ap-card__action--active { + background: color-mix(in srgb, var(--color-red45) 12%, transparent); + color: var(--color-red45); +} + +.ap-card__action--boost.ap-card__action--active { + background: color-mix(in srgb, var(--color-green50) 12%, transparent); + color: var(--color-green50); +} + +.ap-card__action--save.ap-card__action--active { + background: color-mix(in srgb, var(--color-primary) 12%, transparent); + color: var(--color-primary); +} + +.ap-card__action:disabled { + cursor: wait; + opacity: 0.5; +} + +/* Interaction counts */ +.ap-card__count { + font-size: var(--font-size-xs); + color: inherit; + opacity: 0.7; + margin-left: 0.1em; + font-variant-numeric: tabular-nums; +} + +/* Error message */ +.ap-card__action-error { + color: var(--color-error); + font-size: var(--font-size-s); + width: 100%; +} + +/* ========================================================================== + Pagination + ========================================================================== */ + +.ap-pagination { + border-top: var(--border-width-thin) solid var(--color-outline); + display: flex; + gap: var(--space-m); + justify-content: space-between; + margin-top: var(--space-m); + padding-top: var(--space-m); +} + +.ap-pagination a { + color: var(--color-primary-on-background); + text-decoration: none; +} + +.ap-pagination a:hover { + text-decoration: underline; +} + +/* Hidden once Alpine is active (JS replaces with infinite scroll) */ +.ap-pagination--js-hidden { + /* Shown by default for no-JS fallback — Alpine hides via display:none */ +} + +/* ========================================================================== + Infinite Scroll / Load More + ========================================================================== */ + +.ap-load-more { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-s); + padding: var(--space-m) 0; +} + +.ap-load-more__sentinel { + height: 1px; + width: 100%; +} + +.ap-load-more__btn { + background: var(--color-offset); + border: var(--border-width-thin) solid var(--color-outline); + border-radius: var(--border-radius-small); + color: var(--color-on-background); + cursor: pointer; + font-size: var(--font-size-s); + padding: var(--space-xs) var(--space-m); + transition: background 0.15s; +} + +.ap-load-more__btn:hover:not(:disabled) { + background: var(--color-offset-variant); +} + +.ap-load-more__btn:disabled { + cursor: wait; + opacity: 0.6; +} + +.ap-load-more__done { + color: var(--color-on-offset); + font-size: var(--font-size-s); + margin: 0; + text-align: center; +} diff --git a/assets/css/media.css b/assets/css/media.css new file mode 100644 index 0000000..44bf8fe --- /dev/null +++ b/assets/css/media.css @@ -0,0 +1,315 @@ +/* ========================================================================== + Photo Gallery + ========================================================================== */ + +.ap-card__gallery { + border-radius: var(--border-radius-small); + display: grid; + gap: 2px; + margin-bottom: var(--space-s); + overflow: hidden; +} + +.ap-card__gallery-link { + appearance: none; + background: none; + border: 0; + cursor: pointer; + display: block; + padding: 0; + position: relative; +} + +.ap-card__gallery img { + background: var(--color-offset-variant); + display: block; + height: 280px; + object-fit: cover; + width: 100%; + transition: filter 0.2s ease; +} + +@media (max-width: 480px) { + .ap-card__gallery img { + height: 180px; + } +} + +.ap-card__gallery-link:hover img { + filter: brightness(0.92); +} + +.ap-card__gallery-link--more::after { + background: hsl(var(--tint-neutral) 10% / 0.5); + bottom: 0; + content: ""; + left: 0; + position: absolute; + right: 0; + top: 0; +} + +.ap-card__gallery-more { + color: var(--color-neutral99); + font-size: 1.5em; + font-weight: 600; + left: 50%; + position: absolute; + top: 50%; + transform: translate(-50%, -50%); + z-index: 1; +} + +/* 1 photo */ +.ap-card__gallery--1 { + grid-template-columns: 1fr; +} + +.ap-card__gallery--1 img { + height: auto; + max-height: 500px; +} + +/* 2 photos — side by side */ +.ap-card__gallery--2 { + grid-template-columns: 1fr 1fr; +} + +/* 3 photos — one large, two small */ +.ap-card__gallery--3 { + grid-template-columns: 2fr 1fr; + grid-template-rows: 1fr 1fr; +} + +.ap-card__gallery--3 img:first-child { + grid-row: 1 / 3; + height: 100%; +} + +/* 4+ photos — 2x2 grid */ +.ap-card__gallery--4 { + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr 1fr; +} + +/* ========================================================================== + Photo Lightbox + ========================================================================== */ + +[x-cloak] { + display: none !important; +} + +.ap-lightbox { + align-items: center; + background: hsl(var(--tint-neutral) 10% / 0.92); + display: flex; + inset: 0; + justify-content: center; + position: fixed; + z-index: 9999; +} + +.ap-lightbox__img { + max-height: 90vh; + max-width: 95vw; + object-fit: contain; +} + +.ap-lightbox__close { + background: none; + border: 0; + color: white; + cursor: pointer; + font-size: 2rem; + line-height: 1; + padding: var(--space-s); + position: absolute; + right: var(--space-m); + top: var(--space-m); +} + +.ap-lightbox__close:hover { + opacity: 0.7; +} + +.ap-lightbox__prev, +.ap-lightbox__next { + background: none; + border: 0; + color: white; + cursor: pointer; + font-size: 3rem; + line-height: 1; + padding: var(--space-m); + position: absolute; + top: 50%; + transform: translateY(-50%); +} + +.ap-lightbox__prev { + left: var(--space-s); +} + +.ap-lightbox__next { + right: var(--space-s); +} + +.ap-lightbox__prev:hover, +.ap-lightbox__next:hover { + opacity: 0.7; +} + +.ap-lightbox__counter { + bottom: var(--space-m); + color: white; + font-size: var(--font-size-s); + left: 50%; + position: absolute; + transform: translateX(-50%); +} + +/* ========================================================================== + Link Preview Card + ========================================================================== */ + +.ap-link-previews { + margin-bottom: var(--space-s); +} + +.ap-link-preview { + display: flex; + border: var(--border-width-thin) solid var(--color-outline); + border-radius: var(--border-radius-small); + overflow: hidden; + text-decoration: none; + color: inherit; + transition: border-color 0.2s ease; +} + +.ap-link-preview:hover { + border-color: var(--color-primary); +} + +.ap-link-preview__text { + flex: 1; + min-width: 0; + padding: var(--space-s) var(--space-m); + display: flex; + flex-direction: column; + justify-content: center; + gap: 0.2em; +} + +.ap-link-preview__title { + font-weight: 600; + font-size: var(--font-size-s); + margin: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ap-link-preview__desc { + font-size: var(--font-size-s); + color: var(--color-on-offset); + margin: 0; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.ap-link-preview__domain { + font-size: var(--font-size-xs); + color: var(--color-on-offset); + margin: 0; + display: flex; + align-items: center; + gap: 0.3em; +} + +.ap-link-preview__favicon { + width: 14px; + height: 14px; +} + +.ap-link-preview__image { + flex-shrink: 0; + width: 120px; +} + +.ap-link-preview__image img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; +} + +/* ========================================================================== + Video Embed + ========================================================================== */ + +.ap-card__video { + margin-bottom: var(--space-s); +} + +.ap-card__video video { + border-radius: var(--border-radius-small); + max-height: 400px; + width: 100%; +} + +/* ========================================================================== + Audio Player + ========================================================================== */ + +.ap-card__audio { + margin-bottom: var(--space-s); +} + +.ap-card__audio audio { + width: 100%; +} + +/* Gallery items — positioned for ALT badge overlay */ +.ap-card__gallery-item { + position: relative; +} + +/* ALT text badges */ +.ap-media__alt-badge { + position: absolute; + bottom: 0.5rem; + left: 0.5rem; + background: hsl(var(--tint-neutral) 10% / 0.7); + color: var(--color-neutral99); + font-size: 0.65rem; + font-weight: 700; + padding: 0.15rem 0.35rem; + border-radius: var(--border-radius-small); + border: none; + cursor: pointer; + text-transform: uppercase; + letter-spacing: 0.03em; + z-index: 1; +} + +.ap-media__alt-badge:hover { + background: hsl(var(--tint-neutral) 10% / 0.9); +} + +.ap-media__alt-text { + position: absolute; + bottom: 2.2rem; + left: 0.5rem; + right: 0.5rem; + background: hsl(var(--tint-neutral) 10% / 0.85); + color: var(--color-neutral99); + font-size: var(--font-size-s); + padding: 0.5rem; + border-radius: var(--border-radius-small); + max-height: 8rem; + overflow-y: auto; + z-index: 2; +} diff --git a/assets/css/messages.css b/assets/css/messages.css new file mode 100644 index 0000000..7ac6fd4 --- /dev/null +++ b/assets/css/messages.css @@ -0,0 +1,158 @@ +/* ========================================================================== + Messages + ========================================================================== */ + +.ap-messages__layout { + display: grid; + grid-template-columns: 240px 1fr; + gap: var(--space-m); + min-height: 300px; +} + +.ap-messages__sidebar { + border-right: var(--border-width-thin) solid var(--color-outline); + display: flex; + flex-direction: column; + gap: 2px; + padding-right: var(--space-m); + overflow-y: auto; + max-height: 70vh; +} + +.ap-messages__partner { + align-items: center; + border-radius: var(--border-radius-small); + color: var(--color-on-background); + display: flex; + gap: var(--space-s); + padding: var(--space-s); + text-decoration: none; + transition: background 0.15s ease; +} + +.ap-messages__partner:hover { + background: var(--color-offset); +} + +.ap-messages__partner--active { + background: var(--color-offset); + border-left: 3px solid var(--color-primary); + font-weight: var(--font-weight-bold); +} + +.ap-messages__partner-avatar { + flex-shrink: 0; + height: 32px; + position: relative; + width: 32px; +} + +.ap-messages__partner-avatar img { + border-radius: 50%; + height: 100%; + object-fit: cover; + position: absolute; + inset: 0; + width: 100%; + z-index: 1; +} + +.ap-messages__partner-initial { + align-items: center; + background: var(--color-offset-variant); + border-radius: 50%; + color: var(--color-on-offset); + display: flex; + font-size: var(--font-size-s); + height: 100%; + justify-content: center; + width: 100%; +} + +.ap-messages__partner-info { + display: flex; + flex-direction: column; + min-width: 0; + overflow: hidden; +} + +.ap-messages__partner-name { + font-size: var(--font-size-s); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ap-messages__partner-handle { + color: var(--color-on-offset); + font-size: var(--font-size-xs); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ap-messages__content { + min-width: 0; +} + +.ap-message--outbound { + border-left: 3px solid var(--color-primary); +} + +.ap-message .ap-notification__time { + padding-right: var(--space-l); +} + +.ap-message__direction { + color: var(--color-on-offset); + font-size: var(--font-size-s); + margin-right: var(--space-xs); +} + +.ap-message__content { + color: var(--color-on-background); + font-size: var(--font-size-s); + line-height: 1.5; + margin-top: var(--space-xs); +} + +.ap-message__content p { + margin: 0 0 var(--space-xs); +} + +.ap-message__content p:last-child { + margin-bottom: 0; +} + +/* Inline mention links in DM content (Mastodon wraps @user in span inside a link) */ +.ap-message__content .h-card, +.ap-message__content a.mention, +.ap-message__content a span { + display: inline; +} + +.ap-message__content a { + overflow-wrap: break-word; +} + +@media (max-width: 640px) { + .ap-messages__layout { + grid-template-columns: 1fr; + } + + .ap-messages__sidebar { + border-bottom: var(--border-width-thin) solid var(--color-outline); + border-right: none; + flex-direction: row; + max-height: none; + overflow-x: auto; + padding-bottom: var(--space-s); + padding-right: 0; + -webkit-overflow-scrolling: touch; + } + + .ap-messages__partner { + flex-shrink: 0; + white-space: nowrap; + } +} diff --git a/assets/css/moderation.css b/assets/css/moderation.css new file mode 100644 index 0000000..413d9e9 --- /dev/null +++ b/assets/css/moderation.css @@ -0,0 +1,119 @@ +/* ========================================================================== + Moderation + ========================================================================== */ + +.ap-moderation__section { + margin-bottom: var(--space-l); +} + +.ap-moderation__section h2 { + font-size: var(--font-size-l); + margin-bottom: var(--space-s); +} + +.ap-moderation__list { + list-style: none; + margin: 0; + padding: 0; +} + +.ap-moderation__entry { + align-items: center; + border-bottom: var(--border-width-thin) solid var(--color-outline); + display: flex; + gap: var(--space-s); + justify-content: space-between; + padding: var(--space-s) 0; +} + +.ap-moderation__entry a { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ap-moderation__remove { + background: transparent; + border: var(--border-width-thin) solid var(--color-outline); + border-radius: var(--border-radius-small); + color: var(--color-on-offset); + cursor: pointer; + flex-shrink: 0; + font-size: var(--font-size-s); + padding: var(--space-xs) var(--space-s); +} + +.ap-moderation__remove:hover { + border-color: var(--color-error); + color: var(--color-error); +} + +.ap-moderation__add-form { + display: flex; + gap: var(--space-s); +} + +.ap-moderation__input { + background: var(--color-background); + border: var(--border-width-thick) solid var(--color-outline); + border-radius: var(--border-radius-small); + color: var(--color-on-background); + flex: 1; + font-size: var(--font-size-m); + padding: var(--space-xs) var(--space-s); +} + +.ap-moderation__add-btn { + background: var(--color-offset); + border: var(--border-width-thin) solid var(--color-outline); + border-radius: var(--border-radius-small); + color: var(--color-on-background); + cursor: pointer; + font-size: var(--font-size-m); + padding: var(--space-xs) var(--space-m); +} + +.ap-moderation__add-btn:hover { + background: var(--color-offset-variant); +} + +.ap-moderation__add-btn:disabled { + cursor: not-allowed; + opacity: 0.5; +} + +.ap-moderation__error { + color: var(--color-error); + font-size: var(--font-size-s); + margin-top: var(--space-xs); +} + +.ap-moderation__empty { + color: var(--color-on-offset); + font-size: var(--font-size-s); + font-style: italic; +} + +.ap-moderation__hint { + color: var(--color-on-offset); + font-size: var(--font-size-s); + margin-bottom: var(--space-s); +} + +.ap-moderation__filter-toggle { + display: flex; + gap: var(--space-m); +} + +.ap-moderation__radio { + align-items: center; + cursor: pointer; + display: flex; + gap: var(--space-xs); +} + +.ap-moderation__radio input { + accent-color: var(--color-primary); + cursor: pointer; +} diff --git a/assets/css/notifications.css b/assets/css/notifications.css new file mode 100644 index 0000000..9f390a2 --- /dev/null +++ b/assets/css/notifications.css @@ -0,0 +1,191 @@ +/* ========================================================================== + Notifications + ========================================================================== */ + +/* Notifications Toolbar */ +.ap-notifications__toolbar { + display: flex; + gap: var(--space-s); + margin-bottom: var(--space-m); +} + +.ap-notifications__btn { + background: var(--color-offset); + border: var(--border-width-thin) solid var(--color-outline); + border-radius: var(--border-radius-small); + color: var(--color-on-background); + cursor: pointer; + font-size: var(--font-size-s); + padding: var(--space-xs) var(--space-m); + transition: all 0.2s ease; +} + +.ap-notifications__btn:hover { + background: var(--color-offset-variant); + border-color: var(--color-outline-variant); +} + +.ap-notifications__btn--danger { + color: var(--color-error); +} + +.ap-notifications__btn--danger:hover { + border-color: var(--color-error); +} + +.ap-notification { + align-items: flex-start; + background: var(--color-offset); + border: var(--border-width-thin) solid var(--color-outline); + border-radius: var(--border-radius-small); + display: flex; + gap: var(--space-s); + padding: var(--space-m); + position: relative; +} + +.ap-notification--unread { + border-color: var(--color-yellow50); + box-shadow: 0 0 8px 0 hsl(var(--tint-yellow) 50% / 0.3); +} + +.ap-notification__avatar-wrap { + flex-shrink: 0; + position: relative; +} + +.ap-notification__avatar-wrap { + height: 40px; + width: 40px; +} + +.ap-notification__avatar { + border: var(--border-width-thin) solid var(--color-outline); + border-radius: 50%; + height: 40px; + object-fit: cover; + width: 40px; +} + +.ap-notification__avatar-wrap > img { + position: absolute; + inset: 0; + z-index: 1; +} + +.ap-notification__avatar--default { + align-items: center; + background: var(--color-offset-variant); + color: var(--color-on-offset); + display: inline-flex; + font-size: 1.1em; + font-weight: 600; + justify-content: center; +} + +.ap-notification__type-badge { + bottom: -2px; + font-size: 0.75em; + position: absolute; + right: -4px; +} + +.ap-notification__body { + flex: 1; + min-width: 0; +} + +.ap-notification__actor { + font-weight: 600; +} + +.ap-notification__action { + color: var(--color-on-offset); +} + +.ap-notification__target { + color: var(--color-on-offset); + display: block; + font-size: var(--font-size-s); + margin-top: var(--space-xs); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ap-notification__excerpt { + background: var(--color-offset-variant); + border-radius: var(--border-radius-small); + font-size: var(--font-size-s); + margin-top: var(--space-xs); + padding: var(--space-xs) var(--space-s); +} + +.ap-notification__time { + color: var(--color-on-offset); + flex-shrink: 0; + font-size: var(--font-size-xs); +} + +.ap-notification__dismiss { + position: absolute; + right: var(--space-xs); + top: var(--space-xs); +} + +.ap-notification__dismiss-btn { + background: transparent; + border: 0; + border-radius: var(--border-radius-small); + color: var(--color-on-offset); + cursor: pointer; + font-size: var(--font-size-m); + line-height: 1; + padding: 2px 6px; + transition: all 0.2s ease; +} + +.ap-notification__dismiss-btn:hover { + background: var(--color-offset-variant); + color: var(--color-error); +} + +.ap-notification__actions { + display: flex; + gap: var(--space-s); + margin-top: var(--space-s); +} + +.ap-notification__reply-btn, +.ap-notification__thread-btn { + border: var(--border-width-thin) solid var(--color-outline); + border-radius: var(--border-radius-small); + color: var(--color-on-offset); + font-size: var(--font-size-s); + padding: var(--space-xs) var(--space-s); + text-decoration: none; + transition: all 0.2s ease; +} + +.ap-notification__reply-btn:hover, +.ap-notification__thread-btn:hover { + background: var(--color-offset-variant); + border-color: var(--color-outline-variant); + color: var(--color-on-background); +} + +.ap-notification__handle { + color: var(--color-on-offset); + font-size: var(--font-size-s); + margin-left: var(--space-xs); +} + +.ap-notifications__btn--primary { + background: var(--color-primary); + color: var(--color-on-primary, #fff); + text-decoration: none; +} + +.ap-notifications__btn--primary:hover { + opacity: 0.9; +} diff --git a/assets/css/profile.css b/assets/css/profile.css new file mode 100644 index 0000000..e71b9b6 --- /dev/null +++ b/assets/css/profile.css @@ -0,0 +1,308 @@ +/* ========================================================================== + Remote Profile + ========================================================================== */ + +.ap-profile__header { + border-radius: var(--border-radius-small); + height: 200px; + margin-bottom: var(--space-m); + overflow: hidden; +} + +.ap-profile__header-img { + height: 100%; + object-fit: cover; + width: 100%; +} + +.ap-profile__info { + margin-bottom: var(--space-l); +} + +.ap-profile__avatar-wrap { + height: 80px; + margin-bottom: var(--space-s); + position: relative; + width: 80px; +} + +.ap-profile__avatar-wrap > img { + position: absolute; + inset: 0; + z-index: 1; +} + +.ap-profile__avatar { + border: var(--border-width-thickest) solid var(--color-background); + border-radius: 50%; + height: 80px; + object-fit: cover; + width: 80px; +} + +.ap-profile__avatar--placeholder { + align-items: center; + background: var(--color-offset-variant); + color: var(--color-on-offset); + display: flex; + font-size: 2em; + font-weight: 600; + justify-content: center; +} + +.ap-profile__name { + font-size: var(--font-size-xl); + margin-bottom: var(--space-xs); +} + +.ap-profile__handle { + color: var(--color-on-offset); + margin-bottom: var(--space-s); +} + +.ap-profile__bio { + line-height: var(--line-height-prose); + margin-bottom: var(--space-s); +} + +.ap-profile__bio a { + color: var(--color-primary-on-background); +} + +/* Override upstream .mention { display: grid } for bio content */ +.ap-profile__bio .h-card { + display: inline; +} + +.ap-profile__bio .h-card a, +.ap-profile__bio a.u-url.mention { + display: inline; + white-space: nowrap; +} + +.ap-profile__bio .h-card a span, +.ap-profile__bio a.u-url.mention span { + display: inline; +} + +.ap-profile__bio a.mention.hashtag { + display: inline; + white-space: nowrap; +} + +.ap-profile__bio a.mention.hashtag span { + display: inline; +} + +/* Mastodon invisible/ellipsis spans for long URLs in bios */ +.ap-profile__bio .invisible { + display: none; +} + +.ap-profile__bio .ellipsis::after { + content: "…"; +} + +.ap-profile__actions { + display: flex; + flex-wrap: wrap; + gap: var(--space-s); + margin-top: var(--space-m); +} + +.ap-profile__action { + background: transparent; + border: var(--border-width-thin) solid var(--color-outline); + border-radius: var(--border-radius-small); + color: var(--color-on-background); + cursor: pointer; + font-size: var(--font-size-s); + padding: var(--space-xs) var(--space-m); + text-decoration: none; +} + +.ap-profile__action:hover { + background: var(--color-offset); +} + +.ap-profile__action--follow.ap-profile__action--active { + background: var(--color-primary); + border-color: var(--color-primary); + color: var(--color-on-primary, var(--color-neutral99)); +} + +.ap-profile__action--danger:hover { + border-color: var(--color-error); + color: var(--color-error); +} + +.ap-profile__posts { + margin-top: var(--space-l); +} + +.ap-profile__posts h3 { + border-bottom: var(--border-width-thin) solid var(--color-outline); + font-size: var(--font-size-l); + margin-bottom: var(--space-m); + padding-bottom: var(--space-s); +} + +/* ========================================================================== + My Profile — Admin Profile Header + ========================================================================== */ + +.ap-my-profile { + border: var(--border-width-thin) solid var(--color-outline); + border-radius: var(--border-radius-small); + margin-bottom: var(--space-m); + overflow: hidden; +} + +.ap-my-profile__header { + height: 160px; + overflow: hidden; +} + +.ap-my-profile__header-img { + height: 100%; + object-fit: cover; + width: 100%; +} + +.ap-my-profile__info { + padding: var(--space-m); +} + +.ap-my-profile__avatar-wrap { + margin-bottom: var(--space-s); + margin-top: -40px; +} + +.ap-my-profile__avatar { + border: 3px solid var(--color-background); + border-radius: 50%; + height: 72px; + object-fit: cover; + width: 72px; +} + +.ap-my-profile__avatar--placeholder { + align-items: center; + background: var(--color-offset-variant); + color: var(--color-on-offset); + display: flex; + font-size: 1.8em; + font-weight: 600; + justify-content: center; +} + +.ap-my-profile__name { + font-size: var(--font-size-xl); + margin-bottom: 0; +} + +.ap-my-profile__handle { + color: var(--color-on-offset); + font-size: var(--font-size-s); + margin-bottom: var(--space-s); +} + +.ap-my-profile__bio { + line-height: var(--line-height-prose); + margin-bottom: var(--space-s); +} + +.ap-my-profile__bio a { + color: var(--color-primary-on-background); +} + +/* Override upstream .mention { display: grid } for bio content */ +.ap-my-profile__bio .h-card { display: inline; } +.ap-my-profile__bio .h-card a, +.ap-my-profile__bio a.u-url.mention { display: inline; white-space: nowrap; } +.ap-my-profile__bio .h-card a span, +.ap-my-profile__bio a.u-url.mention span { display: inline; } +.ap-my-profile__bio a.mention.hashtag { display: inline; white-space: nowrap; } +.ap-my-profile__bio a.mention.hashtag span { display: inline; } +.ap-my-profile__bio .invisible { display: none; } +.ap-my-profile__bio .ellipsis::after { content: "…"; } + +.ap-my-profile__fields { + border: var(--border-width-thin) solid var(--color-outline); + border-radius: var(--border-radius-small); + margin: var(--space-s) 0; + overflow: hidden; +} + +.ap-my-profile__field { + border-bottom: var(--border-width-thin) solid var(--color-outline); + display: grid; + grid-template-columns: 120px 1fr; +} + +.ap-my-profile__field:last-child { + border-bottom: 0; +} + +.ap-my-profile__field-name { + background: var(--color-offset); + color: var(--color-on-offset); + font-size: var(--font-size-s); + font-weight: 600; + padding: var(--space-xs) var(--space-s); + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.ap-my-profile__field-value { + font-size: var(--font-size-s); + overflow: hidden; + padding: var(--space-xs) var(--space-s); + text-overflow: ellipsis; + white-space: nowrap; +} + +.ap-my-profile__field-value a { + color: var(--color-primary-on-background); +} + +.ap-my-profile__stats { + display: flex; + gap: var(--space-m); + margin-bottom: var(--space-s); +} + +.ap-my-profile__stat { + color: var(--color-on-offset); + font-size: var(--font-size-s); + text-decoration: none; +} + +.ap-my-profile__stat:hover { + color: var(--color-on-background); +} + +.ap-my-profile__stat strong { + color: var(--color-on-background); + font-weight: 600; +} + +.ap-my-profile__edit { + border: var(--border-width-thin) solid var(--color-outline); + border-radius: var(--border-radius-small); + color: var(--color-on-background); + display: inline-block; + font-size: var(--font-size-s); + padding: var(--space-xs) var(--space-m); + text-decoration: none; +} + +.ap-my-profile__edit:hover { + background: var(--color-offset); + border-color: var(--color-outline-variant); +} + +/* When no header image, don't offset avatar */ +.ap-my-profile__info:first-child .ap-my-profile__avatar-wrap { + margin-top: 0; +} diff --git a/assets/css/responsive.css b/assets/css/responsive.css new file mode 100644 index 0000000..3a0d434 --- /dev/null +++ b/assets/css/responsive.css @@ -0,0 +1,33 @@ +/* ========================================================================== + Responsive + ========================================================================== */ + +@media (max-width: 640px) { + .ap-tabs { + gap: 0; + } + + .ap-tab { + padding: var(--space-xs) var(--space-s); + } + + .ap-card__gallery--3 { + grid-template-columns: 1fr 1fr; + grid-template-rows: auto auto; + } + + .ap-card__gallery--3 img:first-child { + grid-column: 1 / 3; + grid-row: 1; + height: 200px; + } + + .ap-card__actions { + gap: var(--space-xs); + } + + .ap-card__action { + font-size: 0.75rem; + padding: var(--space-xs); + } +} diff --git a/assets/css/skeleton.css b/assets/css/skeleton.css new file mode 100644 index 0000000..2a10d66 --- /dev/null +++ b/assets/css/skeleton.css @@ -0,0 +1,74 @@ +/* ========================================================================== + Skeleton Loaders + ========================================================================== */ + +@keyframes ap-skeleton-shimmer { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + +.ap-skeleton { + background: linear-gradient(90deg, + var(--color-offset) 25%, + var(--color-background) 50%, + var(--color-offset) 75%); + background-size: 200% 100%; + animation: ap-skeleton-shimmer 1.5s ease-in-out infinite; + border-radius: var(--border-radius-small); +} + +.ap-card--skeleton { + pointer-events: none; +} + +.ap-card--skeleton .ap-card__author { + display: flex; + align-items: center; + gap: var(--space-s); +} + +.ap-skeleton--avatar { + width: 2.5rem; + height: 2.5rem; + border-radius: 50%; + flex-shrink: 0; +} + +.ap-skeleton-lines { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.4rem; +} + +.ap-skeleton--name { + height: 0.85rem; + width: 40%; +} + +.ap-skeleton--handle { + height: 0.7rem; + width: 25%; +} + +.ap-skeleton-body { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-top: var(--space-s); +} + +.ap-skeleton--line { + height: 0.75rem; + width: 100%; +} + +.ap-skeleton--short { + width: 60%; +} + +.ap-skeleton-group { + display: flex; + flex-direction: column; + gap: var(--space-m); +} diff --git a/assets/reader-interactions.js b/assets/reader-interactions.js new file mode 100644 index 0000000..cf28238 --- /dev/null +++ b/assets/reader-interactions.js @@ -0,0 +1,115 @@ +/** + * Card interaction Alpine.js component. + * Handles like, boost, and save-for-later actions with optimistic UI and + * rollback on failure. + * + * Configured via data-* attributes on the container element (the