mirror of
https://git.shipoclu.com/moon/plspace.git
synced 2026-08-13 02:42:30 +00:00
initial commit
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
# The styling contract
|
||||
|
||||
These stylesheets are the public API of plspace's appearance. They are written
|
||||
so that a user's own CSS can override any of it without fighting the cascade.
|
||||
|
||||
## Rules the app follows
|
||||
|
||||
1. **No CSS framework, no utility classes.** Every class names the *thing* it
|
||||
styles, not how it looks. `.friend-card-photo`, not `.w-16.rounded`.
|
||||
2. **No Svelte scoped styles.** Not one component has a `<style>` block, so
|
||||
Svelte never appends a `.svelte-1a2b3c` hash to a class. What you see in the
|
||||
DOM inspector is what you write in your selector, permanently.
|
||||
3. **Single-class selectors.** Almost nothing here is more specific than one
|
||||
class. No IDs, no `!important`, no long descendant chains. A plain
|
||||
`.blog-entry { … }` in your CSS ties on specificity and wins on order.
|
||||
4. **User CSS goes last.** The theme store re-appends `#user-stylesheet` to the
|
||||
end of `<head>` whenever it changes, so an equal-specificity tie always
|
||||
resolves in your favour. This is why you never need `!important`.
|
||||
5. **Everything visual is a custom property.** No rule hard-codes a colour, font
|
||||
or size. Retinting the entire app is a matter of overriding tokens.
|
||||
|
||||
## Layer 1: tokens
|
||||
|
||||
`tokens.css` declares every colour, font, border and metric on `:root`. This is
|
||||
the intended entry point — it changes the whole app coherently, including parts
|
||||
you haven't looked at.
|
||||
|
||||
```css
|
||||
:root {
|
||||
--ms-chrome-bg: #2d0b3a;
|
||||
--ms-module-header-bg: #4a1a5c;
|
||||
--ms-link: #ff77cc;
|
||||
--ms-font-family: 'Comic Sans MS', Verdana, sans-serif;
|
||||
}
|
||||
```
|
||||
|
||||
The five presets in `src/lib/themes.ts` are written *entirely* at this layer —
|
||||
read them as worked examples.
|
||||
|
||||
Token groups: typography (`--ms-font-*`), page (`--ms-page-*`, `--ms-canvas-*`),
|
||||
links (`--ms-link*`), chrome (`--ms-chrome-*`, `--ms-nav-*`), modules
|
||||
(`--ms-module-*`, `--ms-band-*`), tables (`--ms-table-*`), forms
|
||||
(`--ms-input-*`, `--ms-button-*`), avatars (`--ms-avatar-*`), layout
|
||||
(`--ms-page-width*`, `--ms-column-*`).
|
||||
|
||||
## Layer 2: classes
|
||||
|
||||
When tokens aren't enough, target classes directly.
|
||||
|
||||
| Area | Classes |
|
||||
| --- | --- |
|
||||
| Page skeleton | `.page`, `.page-title`, `.page-subtitle`, `.layout--split`, `.layout--dashboard`, `.layout--single`, `.layout-column--left`, `.layout-column--main`, `.layout-column--right` |
|
||||
| Chrome | `.site-header`, `.site-logo`, `.site-nav`, `.site-nav-link`, `.site-footer` |
|
||||
| Boxes | `.module`, `.module-header`, `.module-body`, `.module--band`, `.module--plain`, `.section-heading` |
|
||||
| Profile | `.profile-page`, `.profile-photo`, `.profile-headline`, `.profile-vitals`, `.profile-mood`, `.profile-badge` |
|
||||
| Tables | `.data-table`, `.data-table-label`, `.data-table-value`, `.interests-table`, `.details-table` |
|
||||
| Friends | `.friend-grid`, `.friend-card`, `.friend-card-name`, `.friend-card-photo`, `.person-row` |
|
||||
| Entries | `.blog-list`, `.blog-entry`, `.blog-entry-header`, `.blog-entry-body`, `.blog-entry-actions`, `.blog-action`, `.entry-teaser` |
|
||||
| Comments | `.comment-list`, `.comment`, `.comment-author`, `.comment-body` |
|
||||
| Media | `.attachment`, `.attachment-media`, `.attachment-caption`, `.preview-card`, `.poll` |
|
||||
| Mail | `.mail-folders`, `.mail-table`, `.mail-row`, `.mail-summary` |
|
||||
| Forms | `.button`, `.field`, `.field-label`, `.link-button`, `.tab-bar`, `.tab` |
|
||||
|
||||
## Layer 3: state attributes
|
||||
|
||||
Rather than inventing a modifier class per combination, elements carry data
|
||||
attributes describing what they *are*. Style by state with attribute selectors:
|
||||
|
||||
```css
|
||||
.blog-entry[data-mine='true'] { background: #fffbe6; }
|
||||
.blog-entry[data-visibility='private'] { background: #fff0f0; }
|
||||
.blog-entry[data-boosted='true'] { opacity: 0.85; }
|
||||
.blog-entry[data-sensitive='true'] { border-left: 3px solid red; }
|
||||
.comment[data-depth='2'] { font-size: 11px; }
|
||||
.mail-row[data-kind='follow_request'] { background: #ffffcc; }
|
||||
.attachment[data-type='video'] { border-color: purple; }
|
||||
.blog-action[aria-pressed='true'] { font-weight: 700; }
|
||||
.site-nav-link[aria-current='page'] { background: orange; }
|
||||
```
|
||||
|
||||
Available attributes: `data-account` (on entries, cards, rows), `data-status-id`,
|
||||
`data-visibility`, `data-boosted`, `data-reply`, `data-sensitive`, `data-mine`,
|
||||
`data-compact`, `data-depth`, `data-kind`, `data-type`, `data-verified`,
|
||||
`data-timeline`, `data-view`, `data-folder`, `data-row`, `data-badge`,
|
||||
`data-private`, `data-session`.
|
||||
|
||||
## There is no dark mode
|
||||
|
||||
Deliberately. A dark theme is a set of token overrides and nothing more, so it
|
||||
lives at layer 1 like any other theme — see the **Midnight** and **Terminal**
|
||||
presets in `src/lib/themes.ts`. Shipping a hardcoded toggle would have meant one
|
||||
dark theme nobody could edit, next to a styling system built for exactly this.
|
||||
|
||||
Two things a dark theme must remember:
|
||||
|
||||
- Set `color-scheme: dark` on `:root` so native form controls and scrollbars
|
||||
follow. Viewer CSS is unscoped, so this works.
|
||||
- Recolour `--ms-chrome-bg`. The logo sits on the chrome band and is knocked
|
||||
out to white by `--ms-logo-filter`, so any dark band works as-is. If you theme
|
||||
the band to a *light* colour, set `--ms-logo-filter: none` to get the mark's
|
||||
own navy back.
|
||||
|
||||
## Publishing a layout on your profile
|
||||
|
||||
Put CSS in a profile field named `css` (or `style`, `layout`, `stylesheet`) on
|
||||
your server. Anyone viewing your profile in plspace gets it. Because it's an
|
||||
ordinary profile field it federates normally — other clients just show it as
|
||||
text.
|
||||
|
||||
Published CSS is **scoped to `.profile-page`** and filtered before it is applied:
|
||||
|
||||
- `@import` is removed (it would pull in an unbounded external stylesheet).
|
||||
- `url()` is allowed only for `https:` and `data:image/` (no tracking pixels).
|
||||
- `position: fixed` becomes `static` (no viewport-covering overlays).
|
||||
- Every selector is prefixed with `.profile-page`, including rules nested inside
|
||||
`@media` / `@supports`. Writing `body` or `html` targets the profile page
|
||||
itself, which is the useful interpretation.
|
||||
- `@keyframes` blocks pass through untouched, so animations still work.
|
||||
|
||||
The net effect: you can do anything to your own page, and nothing to anyone
|
||||
else's or to the surrounding app. Viewers can switch the whole feature off in
|
||||
Settings.
|
||||
|
||||
## Adding styles to the app itself
|
||||
|
||||
Add the rule to the file that owns that area — `chrome.css`, `layout.css`,
|
||||
`module.css`, `profile.css`, `blog.css`, `forms.css` — and register any new
|
||||
colour or metric as a token in `tokens.css` first. Do not introduce a `<style>`
|
||||
block in a component; it would create a hashed class the contract above promises
|
||||
doesn't exist.
|
||||
Reference in New Issue
Block a user