diff --git a/.gitignore b/.gitignore index 319252b..639a628 100644 --- a/.gitignore +++ b/.gitignore @@ -2,12 +2,3 @@ data/ .fsdb* public/files/* .vscode/* - - -# tim -ABOUT.md -AGENTS.md -API.md -ARCHITECTURE.md -CONTRIBUTING.md -REVIEW.md \ No newline at end of file diff --git a/ISSUES.md b/ISSUES.md deleted file mode 100644 index 34a7501..0000000 --- a/ISSUES.md +++ /dev/null @@ -1,68 +0,0 @@ -# Issues - - -## Creating Channels -* Creating a channel is weird from a UX perspective. You hit the plus arrow and a text field comes up, but there isn't any indication of how to submit it. Hitting Enter works, but not having a button is a weird UX choice. -* Hitting the plus icon again hides the text field but doesn't clear the value -* The plus button works as a visibility toggle but doesn't change from + to -. I would add a + button next to the Channels title that opens a mini form with a text field plus Create and Cancel buttons. You can have the Esc key close the dialog, but there should be clear buttons showing what does what. - -## Chatting - -* Attaching files doesn't really show what's happening; there should be a preview, whether link, image, or whatever. -* The chat bar should have a draggable area to drop files into. -* By default it seems 'react' gives a permission error? It also has a weird assortment of emoji's by default. - - -## Blurbs - -* The + button acts as a hide/show toggle but doesn't change to - or whatever when it is shown. -* Hiding a blurb doesn't clear the fields. -* The character count seems to keep the previous count after posting a blurb. -* Need a filter and sort feature so you can see blurbs by topic or user. -* Need a subscription feature with notifications so you can see what a user is saying -* Need a read/unread feature so users can filter out read blurbs -* Is the goal to be able to respond to blurbs? -* Hashtags? -* Sharing and linking to Chats, Essays, and Forums - -## Forum - -* If this is supposed to be like a BBS forum should there be folders and/or topics? -* Need a way to respond -* Need a way to see unread posts -* Need a way to search, filter/subscribe and hide -* Do Forums need admins or owners who can block users, delete posts, etc? -* Sharing and linking to Chats, Essays, and Forums - -## Essays - -* Need a way to see unread posts -* Need a way to search, filter and hide -* Likes? -* Sharing and linking to Blurbs, Chats, and Forums - -## Map - -* Is the point to show where users are? If so users need to be able to set their location. -* Should users only be able to share their location with certain users, or everyone? - - -## General UI - -* Log out button is very large and in a weird spot. Logging out is not a common activity so it shouldn't have such prominence. -* What is the purpose of the left-hand tray? Is it for eventually having access to multiple servers, or is it just a logo? It takes up real estate without providing a use. If the server feature isn't going to exist for a while, it should be removed and the logo placed elsewhere. -* The user's avatar is very large with some properties around it. This should probably follow the more common convention of being an icon in the top right, with the settings in a properties panel or page. Once set, the user isn't going to interact with the profile that much, so more room should be given to common operations. -* There isn't really a theme, or at least the look is very sparse. Setting just a color is fine, but it would be nice to have the ability to set a swatch or theme of different colors, like an IDE theme. -* Context menu icons are rough -* No server admin panel -* Need a way to admin users -* History of user actions -* Global search to find chats, blurbs, forums, or essays - -## Accessibility - - * Might need to look at the tabbing order and make sure everything is tabbable. - * Hotkeys for switching between areas - * Contrast is harsh for different colors in the theme. Colors for buttons, labels, and icons should be calculated to have adequate contrast with the primary color - * Light/dark theme - diff --git a/PERMISSIONS.md b/PERMISSIONS.md deleted file mode 100644 index 036aea1..0000000 --- a/PERMISSIONS.md +++ /dev/null @@ -1,151 +0,0 @@ -# PERMISSIONS.md - -Permission reference for `autonomous.contact`. - -This document was built by crawling the codebase for: - -- default permission assignment in [public/api/users/index.ts](public/api/users/index.ts) -- server-side permission checks in [public/api/](public/api) -- frontend permission gates in [public/tabs/](public/tabs) and related UI files - -## Sources of truth - -Current default permission sets are defined in [public/api/users/index.ts](public/api/users/index.ts#L14-L58). - -- `DEFAULT_USER_PERMISSIONS` are assigned to normal users during signup. -- `DEFAULT_SUPERUSER_PERMISSIONS` are assigned to the first/bootstrap user. - -## How permissions work - -There are two layers of access control in this codebase: - -1. **Global permission strings** stored on the user record. -2. **Object-level ACLs** stored inside resources, especially channel `permissions.read`, `permissions.write`, `permissions.events.read`, and `permissions.events.write`. - -Important consequence: a user may have a global permission and still be blocked by a channel-level ACL. - -## Generic permission families - -These are not stored as standalone permissions, but the server checks for these prefixes: - -- `events.create.*` via [public/api/events/index.ts](public/api/events/index.ts#L188) and [utils/prechecks.ts](utils/prechecks.ts#L47-L48) -- `events.write.*` via [public/api/events/:event_id/index.ts](public/api/events/:event_id/index.ts#L34-L35), [public/api/events/:event_id/index.ts](public/api/events/:event_id/index.ts#L117-L118), and [utils/prechecks.ts](utils/prechecks.ts#L50-L51) - -## Permissions bible - -| Permission | Default user | Bootstrap superuser | Purpose | Where checked | Notes | -| --- | --- | --- | --- | --- | --- | -| `channels.read` | yes | yes | Allows listing channels. | [public/api/channels/index.ts](public/api/channels/index.ts#L12) | Channel detail reads also require channel ACL membership; this permission gates the channel list endpoint only. | -| `channels.create` | no | yes | Allows creating channels. | [public/api/channels/index.ts](public/api/channels/index.ts#L31), [public/tabs/chat/channel_sidebar.html](public/tabs/chat/channel_sidebar.html#L189) | Fully wired. | -| `channels.delete` | no | yes | Intended to allow channel deletion. | Defined in [public/api/users/index.ts](public/api/users/index.ts#L51) | **No direct server-side check found.** Channel deletion currently uses `channel.permissions.write` membership instead of this string. See [public/api/channels/:channel_id/index.ts](public/api/channels/:channel_id/index.ts#L98-L100). | -| `channels.write` | no | yes | Intended to allow channel updates. | Defined in [public/api/users/index.ts](public/api/users/index.ts#L52) | **No direct server-side check found.** Channel updates currently use `channel.permissions.write` membership instead of this string. See [public/api/channels/:channel_id/index.ts](public/api/channels/:channel_id/index.ts#L45-L47). | -| `events.create.blurb` | yes | yes | Allows creating `blurb` events. | [utils/prechecks.ts](utils/prechecks.ts#L47-L48), [public/tabs/blurbs/new_blurb.html](public/tabs/blurbs/new_blurb.html#L28) | Matches server behavior. | -| `events.create.chat` | yes | yes | Allows creating `chat` events. | [utils/prechecks.ts](utils/prechecks.ts#L47-L48) | **UI mismatch:** chat composer is gated by `events.write.chat`, not `events.create.chat`. See [public/tabs/chat/chat.html](public/tabs/chat/chat.html#L151). | -| `events.create.essay` | yes | yes | Allows creating `essay` events. | [utils/prechecks.ts](utils/prechecks.ts#L47-L48), [public/tabs/essays/new_essay.html](public/tabs/essays/new_essay.html#L25) | Matches server behavior. | -| `events.create.post` | yes | yes | Allows creating `post` events. | [utils/prechecks.ts](utils/prechecks.ts#L47-L48), [public/tabs/forum/new_post.html](public/tabs/forum/new_post.html#L9) | Matches server behavior. | -| `events.create.presence` | yes | yes | Intended to allow creating `presence` events. | [utils/prechecks.ts](utils/prechecks.ts#L47-L48) | No dedicated UI or route-specific feature found beyond generic event creation. | -| `events.read.blurb` | yes | yes | Intended to allow viewing blurbs. | [public/tabs/blurbs/blurbs.html](public/tabs/blurbs/blurbs.html#L150) | **Frontend-only gate found.** No matching server-side event-read check by permission string was found. | -| `events.read.chat` | yes | yes | Intended to allow viewing chat. | [public/tabs/chat/chat.html](public/tabs/chat/chat.html#L23) | **Frontend-only gate found.** Server reads rely on authentication and channel ACLs, not this string. | -| `events.read.essay` | yes | yes | Intended to allow viewing essays. | [public/tabs/essays/essays.html](public/tabs/essays/essays.html#L126) | **Frontend-only gate found.** | -| `events.read.post` | yes | yes | Intended to allow viewing posts. | [public/tabs/forum/forum.html](public/tabs/forum/forum.html#L181) | **Frontend-only gate found.** | -| `events.read.presence` | yes | yes | Intended to allow viewing presence events. | Defined in [public/api/users/index.ts](public/api/users/index.ts#L27) | **No active check found** beyond the default assignment. | -| `events.write.blurb` | yes | yes | Allows updating/deleting `blurb` events. | [utils/prechecks.ts](utils/prechecks.ts#L50-L51) | Used by generic event update/delete endpoints. | -| `events.write.chat` | yes | yes | Allows updating/deleting `chat` events. | [utils/prechecks.ts](utils/prechecks.ts#L50-L51), [public/tabs/chat/chat.html](public/tabs/chat/chat.html#L151) | Also used by the chat composer UI, which appears to be stricter/different than server-side create rules. | -| `events.write.essay` | yes | yes | Allows updating/deleting `essay` events. | [utils/prechecks.ts](utils/prechecks.ts#L50-L51) | No dedicated UI gate found. | -| `events.write.post` | yes | yes | Allows updating/deleting `post` events. | [utils/prechecks.ts](utils/prechecks.ts#L50-L51) | No dedicated UI gate found. | -| `events.write.presence` | yes | yes | Intended to allow updating/deleting `presence` events. | [utils/prechecks.ts](utils/prechecks.ts#L50-L51) | No dedicated UI or route-specific usage found beyond generic event mutation logic. | -| `files.write.own` | yes | yes | Allows uploads only inside the current user's home path under `/files/users//...`. | [public/_pre.ts](public/_pre.ts#L12-L20) | Fully wired. | -| `files.write.all` | no | yes | Allows uploads anywhere under `/files/...`. | [public/_pre.ts](public/_pre.ts#L13-L20), [tests/11_file_uploads.test.ts](tests/11_file_uploads.test.ts#L165) | Fully wired. | -| `invites.create` | yes | yes | Allows creating invite codes. | [public/api/users/:user_id/invites/index.ts](public/api/users/:user_id/invites/index.ts#L73) | Fully wired. | -| `invites.read.own` | yes | yes | Allows reading invites for the path user when it is self. | [public/api/users/:user_id/invites/index.ts](public/api/users/:user_id/invites/index.ts#L17-L20) | Route behavior currently has filtering issues; see [REVIEW.md](REVIEW.md). | -| `invites.read.all` | no | yes | Allows reading invites across users. | [public/api/users/:user_id/invites/index.ts](public/api/users/:user_id/invites/index.ts#L18-L20) | Route behavior currently has filtering issues; see [REVIEW.md](REVIEW.md). | -| `self.read` | yes | yes | Allows reading the current authenticated user's own profile. | [public/api/users/me/index.ts](public/api/users/me/index.ts#L8), [public/api/users/:user_id/index.ts](public/api/users/:user_id/index.ts#L13-L17) | Fully wired. | -| `self.write` | yes | yes | Allows updating/deleting the current user's own profile. | [public/api/users/:user_id/index.ts](public/api/users/:user_id/index.ts#L44-L48), [public/api/users/:user_id/index.ts](public/api/users/:user_id/index.ts#L109-L113) | Fully wired. | -| `signups.read.own` | yes | yes | Allows reading signups for the path user when it is self. | [public/api/users/:user_id/signups/index.ts](public/api/users/:user_id/signups/index.ts#L13-L16) | Route behavior currently has filtering issues; see [REVIEW.md](REVIEW.md). | -| `signups.read.all` | no | yes | Allows reading signups across users. | [public/api/users/:user_id/signups/index.ts](public/api/users/:user_id/signups/index.ts#L14-L16) | Route behavior currently has filtering issues; see [REVIEW.md](REVIEW.md). | -| `users.read` | yes | yes | Allows reading/searching other users. | [public/api/users/index.ts](public/api/users/index.ts#L67), [public/api/users/:user_id/index.ts](public/api/users/:user_id/index.ts#L14-L17) | Fully wired. | -| `users.write` | no | yes | Allows updating/deleting other users and editing permissions. | [public/api/users/:user_id/index.ts](public/api/users/:user_id/index.ts#L45), [public/api/users/:user_id/index.ts](public/api/users/:user_id/index.ts#L81), [public/api/users/:user_id/index.ts](public/api/users/:user_id/index.ts#L110) | Fully wired. | -| `watches.create.own` | yes | yes | Allows creating watches for self. | [public/api/users/:user_id/watches/index.ts](public/api/users/:user_id/watches/index.ts#L80-L83) | Fully wired. | -| `watches.create.all` | no | no | Allows creating watches for other users. | [public/api/users/:user_id/watches/index.ts](public/api/users/:user_id/watches/index.ts#L81-L83) | **Referenced but not granted by either default permission set.** Must be assigned manually if needed. | -| `watches.read.own` | yes | yes | Allows reading watches for self. | [public/api/users/:user_id/watches/index.ts](public/api/users/:user_id/watches/index.ts#L15-L18) | Route behavior currently has filtering issues; see [REVIEW.md](REVIEW.md). | -| `watches.read.all` | no | yes | Allows reading watches across users. | [public/api/users/:user_id/watches/index.ts](public/api/users/:user_id/watches/index.ts#L16-L18) | Route behavior currently has filtering issues; see [REVIEW.md](REVIEW.md). | -| `watches.write.own` | yes | yes | Intended to allow updating/deleting own watches. | Defined in [public/api/users/index.ts](public/api/users/index.ts#L44) | **No direct permission check found.** Watch update/delete currently use ownership only. See [public/api/users/:user_id/watches/:watch_id/index.ts](public/api/users/:user_id/watches/:watch_id/index.ts#L20-L22) and [public/api/users/:user_id/watches/:watch_id/index.ts](public/api/users/:user_id/watches/:watch_id/index.ts#L73-L75). | -| `watches.write.all` | no | yes | Intended to allow updating/deleting others' watches. | Defined in [public/api/users/index.ts](public/api/users/index.ts#L58) | **No direct permission check found.** Watch update/delete currently use ownership only. | - -## Summary: default permissions vs actual code - -### Normal-user defaults that are clearly used - -These are present in `DEFAULT_USER_PERMISSIONS` and have matching checks in code: - -- `channels.read` -- `events.create.blurb` -- `events.create.chat` -- `events.create.essay` -- `events.create.post` -- `events.create.presence` -- `events.write.blurb` -- `events.write.chat` -- `events.write.essay` -- `events.write.post` -- `events.write.presence` -- `files.write.own` -- `invites.create` -- `invites.read.own` -- `self.read` -- `self.write` -- `signups.read.own` -- `users.read` -- `watches.create.own` -- `watches.read.own` - -### Defaults that are only partially matched or frontend-only - -- `events.read.blurb` -- `events.read.chat` -- `events.read.essay` -- `events.read.post` -- `events.read.presence` -- `watches.write.own` - -### Bootstrap/superuser defaults that are present but not fully wired as permission strings - -- `channels.delete` -- `channels.write` -- `watches.write.all` - -### Referenced in code but not granted by default - -- `watches.create.all` - -## Biggest mismatches to know about - -1. **Channel update/delete do not use `channels.write` or `channels.delete`.** - They use per-channel ACL membership instead. See [public/api/channels/:channel_id/index.ts](public/api/channels/:channel_id/index.ts). - -2. **Watch update/delete do not use `watches.write.own` or `watches.write.all`.** - They use ownership checks only. See [public/api/users/:user_id/watches/:watch_id/index.ts](public/api/users/:user_id/watches/:watch_id/index.ts). - -3. **`events.read.*` permissions are mostly UI gates, not server-enforced authorization.** - The event read endpoints do not generally check these strings. - -4. **Chat creation UI uses `events.write.chat`, but the server requires `events.create.chat` for POST.** - This is the clearest create-vs-write mismatch in the current code. - -5. **`watches.create.all` exists in code but is missing from both default sets.** - -## Recommendation - -If the goal is to make permissions predictable, the cleanest next step would be to choose one of these approaches: - -1. remove unused permission strings from defaults, or -2. add explicit checks so every documented permission is actually authoritative - -The most important cleanup targets are: - -- `channels.write` -- `channels.delete` -- `watches.write.own` -- `watches.write.all` -- the `events.read.*` family -- the chat UI mismatch between `events.create.chat` and `events.write.chat` diff --git a/README.md b/README.md index d3f86f6..b83bfc0 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ feature discussions. 3) Start the server: - `deno run serve` + `deno run task serve` 4) Navigate to http://localhost:8000 diff --git a/deno.json b/deno.json index a4b793c..beda9ac 100644 --- a/deno.json +++ b/deno.json @@ -46,10 +46,10 @@ "@andyburke/lurid": "jsr:@andyburke/lurid@^0.2.0", "@andyburke/serverus": "jsr:@andyburke/serverus@^0.16.0", "@da/bcrypt": "jsr:@da/bcrypt@^1.0.1", - "@std/assert": "jsr:@std/assert@^1.0.17", + "@std/assert": "jsr:@std/assert@^1.0.19", "@std/encoding": "jsr:@std/encoding@^1.0.10", - "@std/fs": "jsr:@std/fs@^1.0.22", - "@std/http": "jsr:@std/http@^1.0.23", + "@std/fs": "jsr:@std/fs@^1.0.23", + "@std/http": "jsr:@std/http@^1.0.25", "@std/media-types": "jsr:@std/media-types@^1.1.0", "@std/path": "jsr:@std/path@^1.1.4" } diff --git a/deno.lock b/deno.lock index 0e06569..e974966 100644 --- a/deno.lock +++ b/deno.lock @@ -6,28 +6,27 @@ "jsr:@andyburke/serverus@0.16": "0.16.0", "jsr:@da/bcrypt@*": "1.0.1", "jsr:@da/bcrypt@^1.0.1": "1.0.1", - "jsr:@std/assert@^1.0.17": "1.0.17", - "jsr:@std/cli@^1.0.19": "1.0.25", - "jsr:@std/cli@^1.0.20": "1.0.25", - "jsr:@std/cli@^1.0.21": "1.0.25", - "jsr:@std/cli@^1.0.25": "1.0.25", + "jsr:@std/assert@^1.0.19": "1.0.19", + "jsr:@std/cli@^1.0.19": "1.0.28", + "jsr:@std/cli@^1.0.20": "1.0.28", + "jsr:@std/cli@^1.0.21": "1.0.28", + "jsr:@std/cli@^1.0.28": "1.0.28", "jsr:@std/encoding@^1.0.10": "1.0.10", - "jsr:@std/fmt@^1.0.6": "1.0.8", - "jsr:@std/fmt@^1.0.8": "1.0.8", - "jsr:@std/fs@^1.0.18": "1.0.22", - "jsr:@std/fs@^1.0.19": "1.0.22", - "jsr:@std/fs@^1.0.21": "1.0.22", - "jsr:@std/fs@^1.0.22": "1.0.22", + "jsr:@std/fmt@^1.0.6": "1.0.9", + "jsr:@std/fmt@^1.0.9": "1.0.9", + "jsr:@std/fs@^1.0.18": "1.0.23", + "jsr:@std/fs@^1.0.19": "1.0.23", + "jsr:@std/fs@^1.0.23": "1.0.23", "jsr:@std/html@^1.0.5": "1.0.5", - "jsr:@std/http@^1.0.20": "1.0.23", - "jsr:@std/http@^1.0.23": "1.0.23", + "jsr:@std/http@^1.0.20": "1.0.25", + "jsr:@std/http@^1.0.25": "1.0.25", "jsr:@std/internal@^1.0.12": "1.0.12", "jsr:@std/media-types@^1.1.0": "1.1.0", "jsr:@std/net@^1.0.6": "1.0.6", "jsr:@std/path@^1.1.0": "1.1.4", "jsr:@std/path@^1.1.1": "1.1.4", "jsr:@std/path@^1.1.4": "1.1.4", - "jsr:@std/streams@^1.0.16": "1.0.16", + "jsr:@std/streams@^1.0.17": "1.0.17", "npm:@types/node@*": "22.15.15" }, "jsr": { @@ -59,23 +58,23 @@ "@da/bcrypt@1.0.1": { "integrity": "d2172d3acbcff52e0465557a1a48b1ff1c92df08c90712dae5372255a8c45eb3" }, - "@std/assert@1.0.17": { - "integrity": "df5ebfffe77c03b3fa1401e11c762cc8f603d51021c56c4d15a8c7ab45e90dbe", + "@std/assert@1.0.19": { + "integrity": "eaada96ee120cb980bc47e040f82814d786fe8162ecc53c91d8df60b8755991e", "dependencies": [ "jsr:@std/internal" ] }, - "@std/cli@1.0.25": { - "integrity": "1f85051b370c97a7a9dfc6ba626e7ed57a91bea8c081597276d1e78d929d8c91" + "@std/cli@1.0.28": { + "integrity": "74ef9b976db59ca6b23a5283469c9072be6276853807a83ec6c7ce412135c70a" }, "@std/encoding@1.0.10": { "integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1" }, - "@std/fmt@1.0.8": { - "integrity": "71e1fc498787e4434d213647a6e43e794af4fd393ef8f52062246e06f7e372b7" + "@std/fmt@1.0.9": { + "integrity": "2487343e8899fb2be5d0e3d35013e54477ada198854e52dd05ed0422eddcabe0" }, - "@std/fs@1.0.22": { - "integrity": "de0f277a58a867147a8a01bc1b181d0dfa80bfddba8c9cf2bacd6747bcec9308", + "@std/fs@1.0.23": { + "integrity": "3ecbae4ce4fee03b180fa710caff36bb5adb66631c46a6460aaad49515565a37", "dependencies": [ "jsr:@std/internal", "jsr:@std/path@^1.1.4" @@ -84,13 +83,13 @@ "@std/html@1.0.5": { "integrity": "4e2d693f474cae8c16a920fa5e15a3b72267b94b84667f11a50c6dd1cb18d35e" }, - "@std/http@1.0.23": { - "integrity": "6634e9e034c589bf35101c1b5ee5bbf052a5987abca20f903e58bdba85c80dee", + "@std/http@1.0.25": { + "integrity": "577b4252290af1097132812b339fffdd55fb0f4aeb98ff11bdbf67998aa17193", "dependencies": [ - "jsr:@std/cli@^1.0.25", + "jsr:@std/cli@^1.0.28", "jsr:@std/encoding", - "jsr:@std/fmt@^1.0.8", - "jsr:@std/fs@^1.0.21", + "jsr:@std/fmt@^1.0.9", + "jsr:@std/fs@^1.0.23", "jsr:@std/html", "jsr:@std/media-types", "jsr:@std/net", @@ -113,8 +112,8 @@ "jsr:@std/internal" ] }, - "@std/streams@1.0.16": { - "integrity": "85030627befb1767c60d4f65cb30fa2f94af1d6ee6e5b2515b76157a542e89c4" + "@std/streams@1.0.17": { + "integrity": "7859f3d9deed83cf4b41f19223d4a67661b3d3819e9fc117698f493bf5992140" } }, "npm": { @@ -137,10 +136,10 @@ "jsr:@andyburke/lurid@0.2", "jsr:@andyburke/serverus@0.16", "jsr:@da/bcrypt@^1.0.1", - "jsr:@std/assert@^1.0.17", + "jsr:@std/assert@^1.0.19", "jsr:@std/encoding@^1.0.10", - "jsr:@std/fs@^1.0.22", - "jsr:@std/http@^1.0.23", + "jsr:@std/fs@^1.0.23", + "jsr:@std/http@^1.0.25", "jsr:@std/media-types@^1.1.0", "jsr:@std/path@^1.1.4" ] diff --git a/public/api/auth/index.ts b/public/api/auth/index.ts index d1b0d58..7d47e32 100644 --- a/public/api/auth/index.ts +++ b/public/api/auth/index.ts @@ -122,8 +122,7 @@ export async function POST(req: Request, meta: Record): Promise): Promise): Response | undefined => { const can_create_channels = meta.user.permissions.includes('channels.create'); - console.log('User permissions:', meta.user.permissions); + if (!can_create_channels) { return CANNED_RESPONSES.permission_denied(); } diff --git a/public/api/events/index.ts b/public/api/events/index.ts index 2bf633b..41b5aa8 100644 --- a/public/api/events/index.ts +++ b/public/api/events/index.ts @@ -41,13 +41,6 @@ export async function GET(request: Request, meta: Record): Promise< event_id } = /^.*\/events\/.*\/(?.*?)\:(?[A-Za-z-]+)\.json$/.exec(entry.path)?.groups ?? {}; - console.dir({ - entry, - event_type, - event_id, - query: meta.query - }); - if (meta.query.after_id && event_id <= meta.query.after_id) { return false; } diff --git a/public/api/users/index.ts b/public/api/users/index.ts index 6730626..a8db076 100644 --- a/public/api/users/index.ts +++ b/public/api/users/index.ts @@ -139,6 +139,22 @@ export async function POST(req: Request, meta: Record): Promise): Promise - - -

Hello World - foo

- - diff --git a/public/icons.css b/public/icons.css new file mode 100644 index 0000000..f3b008e --- /dev/null +++ b/public/icons.css @@ -0,0 +1,1329 @@ +/* ICONS */ +.icon { + width: 24px; + height: 24px; + transform: scale(var(--icon-scale, 1)); + + stroke: white; + fill: transparent; + stroke-width: 1pt; + stroke-miterlimit: 10; + stroke-linecap: round; + stroke-linejoin: round; + stroke-dasharray: 400; + + margin: 0 auto; +} + +/* ICON - ADD (with box) */ +.icon.add { + box-sizing: border-box; + position: relative; + display: block; + width: 22px; + height: 22px; + border: 2px solid; + transform: scale(var(--icon-scale, 1)); + border-radius: 4px; +} + +.icon.add::after, +.icon.add::before { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; + width: 10px; + height: 2px; + background: currentColor; + border-radius: 5px; + top: 8px; + left: 4px; +} + +.icon.add::after { + width: 2px; + height: 10px; + top: 4px; + left: 8px; +} + +/* ICON - ATTACHMENT */ +.icon.attachment { + box-sizing: border-box; + position: relative; + display: block; + width: 14px; + height: 14px; + border: 2px solid; + border-top: 0; + border-bottom-left-radius: 100px; + border-bottom-right-radius: 100px; + transform: scale(var(--icon-scale, 1)); + margin-top: 11px; +} + +.icon.attachment::after, +.icon.attachment::before { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; + border-radius: 3px; + border: 2px solid; +} + +.icon.attachment::after { + border-bottom: 0; + border-top-left-radius: 100px; + border-top-right-radius: 100px; + right: -2px; + width: 10px; + height: 14px; + bottom: 8px; +} + +.icon.attachment::before { + width: 6px; + height: 12px; + border-top: 0; + border-bottom-left-radius: 100px; + border-bottom-right-radius: 100px; + left: 2px; + bottom: 4px; +} + +/* ICON - BLURB */ +.icon.blurb { + box-sizing: border-box; + position: relative; + display: block; + transform: scale(var(--icon-scale, 1)); + border: 2px solid; + border-radius: 3px; + width: 22px; + height: 16px; +} + +.icon.blurb::after, +.icon.blurb::before { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; + height: 2px; + border-radius: 3px; + background: currentColor; + bottom: 2px; +} + +.icon.blurb::before { + width: 10px; + left: 2px; + box-shadow: 4px -4px 0; +} + +.icon.blurb::after { + width: 3px; + right: 2px; + box-shadow: -11px -4px 0; +} + +/* ICON - CALENDAR */ +.icon.calendar, +.icon.calendar::before { + display: block; + box-sizing: border-box; +} + +.icon.calendar { + position: relative; + transform: scale(var(--icon-scale, 1)); + width: 18px; + height: 18px; + border: 2px solid; + border-top: 4px solid; + border-radius: 3px; +} + +.icon.calendar::before { + content: ""; + position: absolute; + width: 10px; + border-radius: 3px; + left: 2px; + background: currentColor; + height: 2px; + top: 2px; +} + +/* ICON - CAMERA */ +.icon.camera { + box-sizing: border-box; + position: relative; + display: block; + transform: scale(var(--icon-scale, 1)); + border: 2px solid; + border-radius: 3px; + width: 18px; + height: 12px; + perspective: 24px; +} + +.icon.camera::after, +.icon.camera::before { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; +} + +.icon.camera::before { + border: 2px solid; + border-left-color: transparent; + transform: rotateY(-70deg); + width: 8px; + height: 8px; + right: -7px; + top: 0; +} + +.icon.camera::after { + width: 10px; + height: 5px; + border-top: 2px solid; + border-right: 2px solid; + top: -5px; + right: 2px; + border-top-right-radius: 2px; +} + +/* ICON - CHAT */ +.icon.chat { + box-sizing: border-box; + position: relative; + display: block; + transform: scale(var(--icon-scale, 1)); + width: 14px; + height: 10px; +} + +.icon.chat::after, +.icon.chat::before { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; + border-radius: 3px; + height: 2px; + background: currentColor; +} + +.icon.chat::before { + width: 10px; + opacity: 0.5; + box-shadow: 0 4px 0; +} + +.icon.chat::after { + width: 14px; + bottom: 0; +} + +/* ICON - CIRCLE */ +.icon.circle { + box-sizing: border-box; + position: relative; + display: block; + transform: scale(var(--icon-scale, 1)); + width: 19px; + height: 19px; + border: 2px solid; + border-radius: 100px; +} + +/* ICON - CLOSE */ +.icon.close { + box-sizing: border-box; + position: relative; + display: block; + transform: scale(var(--icon-scale, 1)); + width: 22px; + height: 22px; + border: 2px solid; + border-radius: var(--border-radius); +} + +.icon.close::after, +.icon.close::before { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; + width: 12px; + height: 2px; + background: currentColor; + transform: rotate(45deg); + border-radius: var(--border-radius); + top: 8px; + left: 3px; +} + +.icon.close::after { + transform: rotate(-45deg); +} + +/* ICON - CONTROLLER */ +.icon.controller { + box-sizing: border-box; + position: relative; + display: block; + transform: scale(var(--icon-scale, 1)); + width: 8px; + height: 8px; + border: 2px solid; + border-radius: 100px; +} + +.icon.controller::before { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; + width: 14px; + height: 14px; + box-shadow: + -6px -6px 0 -4px, + 6px 6px 0 -4px, + 6px -6px 0 -4px, + -6px 6px 0 -4px; + left: -5px; + top: -5px; + transform: rotate(45deg); +} + +/* ICON - DOWNLOAD */ +.icon.download { + box-sizing: border-box; + position: relative; + display: block; + transform: scale(var(--icon-scale, 1)); + width: 16px; + height: 6px; + border: 2px solid; + border-top: 0; + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; + margin-top: 8px; +} + +.icon.download::after { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; + width: 8px; + height: 8px; + border-left: 2px solid; + border-bottom: 2px solid; + transform: rotate(-45deg); + left: 2px; + bottom: 4px; +} + +.icon.download::before { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; + border-radius: 3px; + width: 2px; + height: 10px; + background: currentColor; + left: 5px; + bottom: 5px; +} + +/* ICON - ESSAY */ +.icon.essay { + box-sizing: border-box; + position: relative; + display: block; + transform: scale(var(--icon-scale, 1)); + width: 22px; + height: 18px; + border: 2px solid; + border-radius: 3px; + box-shadow: 0 -1px 0; +} + +.icon.essay::after, +.icon.essay::before { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; + width: 6px; + top: 2px; +} + +.icon.essay::before { + background: currentColor; + left: 2px; + box-shadow: + 0 4px 0, + 0 8px 0; + border-radius: 3px; + height: 2px; +} + +.icon.essay::after { + height: 10px; + border: 2px solid; + right: 2px; + border-radius: 1px; +} + +/* ICON - EXCHANGE */ +.icon.exchange, +.icon.exchange::after, +.icon.exchange::before { + display: block; + box-sizing: border-box; + width: 8px; + height: 8px; +} + +.icon.exchange { + position: relative; + transform: scale(var(--icon-scale, 1)); + box-shadow: + -3px 3px 0 -1px, + 3px -3px 0 -1px; +} + +.icon.exchange::after, +.icon.exchange::before { + content: ""; + position: absolute; + border: 2px solid; +} + +.icon.exchange::before { + top: -5px; + left: -5px; +} + +.icon.exchange::after { + bottom: -5px; + right: -5px; +} + +/* ICON - FORUM */ +.icon.forum { + box-sizing: border-box; + position: relative; + display: block; + transform: scale(var(--icon-scale, 1)); + width: 16px; + height: 14px; + border-bottom: 2px solid; +} + +.icon.forum::after, +.icon.forum::before { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; + top: 2px; +} + +.icon.forum::before { + border-left: 4px solid; + left: 1px; + width: 0; + height: 0; + border-top: 3px solid transparent; + border-bottom: 3px solid transparent; +} + +.icon.forum::after { + width: 8px; + height: 6px; + border-top: 2px solid; + border-bottom: 2px solid; + right: 0; +} + +/* ICON - FORWARD */ +.icon.forward-copy { + box-sizing: border-box; + position: relative; + display: block; + transform: scale(var(--icon-scale, 1)); + width: 16px; + height: 16px; + box-shadow: 6px -6px 0 -4px; +} + +.icon.forward-copy::before { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; + width: 8px; + height: 8px; + border: 2px solid; + border-radius: 1px; + left: 0; + bottom: 0; +} + +.icon.forward-copy::after { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; + background: currentColor; + width: 2px; + height: 10px; + transform: rotate(45deg); + bottom: 8px; + right: 2px; + border-radius: 4px; +} + +/* ICON - HOME */ +.icon.home { + background: + linear-gradient(to left, currentColor 5px, transparent 0) no-repeat 0 bottom/4px 2px, + linear-gradient(to left, currentColor 5px, transparent 0) no-repeat right bottom/4px 2px; + box-sizing: border-box; + position: relative; + display: block; + transform: scale(var(--icon-scale, 1)); + width: 18px; + height: 14px; + border: 2px solid; + border-top: 0; + border-bottom: 0; + border-top-right-radius: 3px; + border-top-left-radius: 3px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + margin-bottom: -2px; +} + +.icon.home::after, +.icon.home::before { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; +} + +.icon.home::before { + border-top: 2px solid; + border-left: 2px solid; + border-top-left-radius: 4px; + transform: rotate(45deg); + top: -5px; + border-radius: 3px; + width: 14px; + height: 14px; + left: 0; +} + +.icon.home::after { + width: 8px; + height: 10px; + border: 2px solid; + border-radius: 100px; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + border-bottom: 0; + left: 3px; + bottom: 0; +} + +/* ICON - LIVE */ +.icon.live { + transform: scale(var(--icon-scale, 1)); +} + +.icon.live, +.icon.live::after, +.icon.live::before { + box-sizing: border-box; + position: relative; + display: block; + width: 14px; + height: 14px; + border: 2px solid; + border-bottom-color: transparent; + border-radius: 50%; +} + +.icon.live::after, +.icon.live::before { + content: ""; + position: absolute; + width: 6px; + height: 6px; + top: 2px; + left: 2px; +} + +.icon.live::after { + width: 22px; + height: 22px; + top: -6px; + left: -6px; +} + +/* ICON - MIC */ +.icon.mic { + box-sizing: border-box; + position: relative; + display: block; + transform: scale(var(--icon-scale, 1)); + width: 16px; + height: 12px; + border-bottom-left-radius: 120px; + border-bottom-right-radius: 120px; + border: 2px solid; + border-top: 0; + margin-top: 3px; +} + +.icon.mic::after, +.icon.mic::before { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; +} + +.icon.mic::after { + border: 2px solid; + width: 8px; + height: 18px; + left: 2px; + top: -10px; + border-radius: 4px; +} + +.icon.mic::before { + width: 10px; + height: 4px; + top: 12px; + left: 1px; + border-right: 4px solid transparent; + box-shadow: + 0 2px 0, + inset -2px 0 0; +} + +/* ICON - MINUS */ +.icon.minus { + box-sizing: border-box; + position: relative; + display: block; + transform: scale(var(--icon-scale, 1)); + width: 16px; + height: 2px; + background: currentColor; + border-radius: 10px; +} + +/* ICON - MORE */ +.icon.more { + box-sizing: border-box; + position: relative; + display: block; + transform: scale(var(--icon-scale, 1)); + width: 24px; + height: 20px; + border: 2px solid; + border-radius: 3px; +} + +.icon.more::before { + content: ""; + position: absolute; + box-sizing: border-box; + display: block; + width: 4px; + height: 4px; + background-color: currentColor; + border-radius: 20px; + top: 6px; + left: 8px; + box-shadow: + -5px 0 0, + 5px 0 0; +} + +/* ICON - MOREBORDERLESS */ +.icon.more-borderless { + transform: scale(var(--icon-scale, 1)); +} + +.icon.more-borderless, +.icon.more-borderless::after, +.icon.more-borderless::before { + box-sizing: border-box; + position: relative; + display: block; + width: 4px; + height: 4px; + background: currentColor; + border-radius: 100%; +} + +.icon.more-borderless::after, +.icon.more-borderless::before { + content: ""; + position: absolute; + top: 0; +} + +.icon.more-borderless::after { + left: -6px; +} + +.icon.more-borderless::before { + right: -6px; +} + +/* ICON - MORECIRCLE */ +.icon.more-circle { + box-sizing: border-box; + position: relative; + display: block; + transform: scale(var(--icon-scale, 1)); + width: 24px; + height: 24px; + border: 2px solid; + border-radius: 24px; +} + +.icon.more-circle::before { + content: ""; + position: absolute; + box-sizing: border-box; + display: block; + width: 4px; + height: 4px; + background-color: currentColor; + border-radius: 20px; + top: 8px; + left: 8px; + box-shadow: + -5px 0 0, + 5px 0 0; +} + +/* ICON - PHONE */ +.icon.phone { + box-sizing: border-box; + position: relative; + display: block; + width: 22px; + height: 22px; + transform: scale(var(--icon-scale, 1)); +} + +.icon.phone::after, +.icon.phone::before { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; +} + +.icon.phone::after { + width: 18px; + height: 18px; + border-top-left-radius: 1px; + border-bottom-right-radius: 1px; + border-bottom-left-radius: 12px; + border-left: 4px solid; + border-bottom: 4px solid; + left: 2px; + bottom: 2px; + background: + linear-gradient(to left, currentColor 10px, transparent 0) no-repeat right 11px/6px 4px, + linear-gradient(to left, currentColor 10px, transparent 0) no-repeat -1px 0/4px 6px; +} + +.icon.phone::before { + width: 20px; + height: 20px; + border: 6px double; + border-top-color: transparent; + border-bottom-color: transparent; + border-left-color: transparent; + border-radius: 50%; + transform: rotate(-45deg); + bottom: 2px; + left: 2px; +} + +/* ICON - PLUS */ +.icon.plus, +.icon.plus::after, +.icon.plus::before { + display: block; + box-sizing: border-box; +} + +.icon.plus::after, +.icon.plus::before { + border-radius: 10px; + background: currentColor; +} + +.icon.plus { + position: relative; + transform: scale(var(--icon-scale, 1)); + width: 16px; + height: 16px; +} + +.icon.plus::after { + content: ""; + position: absolute; + width: 2px; + height: 16px; + top: 0; + left: 7px; +} + +.icon.plus::before { + content: ""; + position: absolute; + width: 16px; + height: 2px; + top: 7px; + left: 0; +} + +/* ICON - REPLY */ +.icon.reply { + box-sizing: border-box; + position: relative; + display: block; + transform: scale(var(--icon-scale, 1)); + width: 16px; + height: 16px; + box-shadow: -6px -6px 0 -4px; +} + +.icon.reply::before { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; + width: 8px; + height: 8px; + border: 2px solid; + border-radius: 1px; + right: 0; + bottom: 0; +} + +.icon.reply::after { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; + background: currentColor; + width: 2px; + height: 10px; + transform: rotate(-45deg); + bottom: 8px; + left: 2px; + border-radius: 4px; +} + +/* ICON - RESOURCES */ +.icon.resources, +.icon.resources::after { + display: block; + box-sizing: border-box; + border-radius: 22px; +} + +.icon.resources { + position: relative; + transform: scale(var(--icon-scale, 1)); + width: 20px; + height: 20px; + border: 2px solid transparent; +} + +.icon.resources::after { + content: ""; + position: absolute; + width: 4px; + height: 4px; + background: currentColor; + top: 6px; + left: 6px; + box-shadow: + 0 7px 0 1px, + 0 -7px 0 1px, + -7px 0 0 1px, + 7px 0 0 1px; +} + +/* ICON - SEND */ +.icon.send { + box-sizing: border-box; + position: relative; + display: block; + transform: scale(var(--icon-scale, 1)); + width: 22px; + height: 22px; + border: 2px solid; + border-radius: 4px; +} + +.icon.send::after, +.icon.send::before { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; + width: 2px; + height: 8px; + border-right: 2px solid; + top: 5px; + right: 5px; +} + +.icon.send::after { + width: 6px; + height: 6px; + border-bottom: 2px solid; + transform: rotate(-45deg); + right: 9px; + top: 6px; +} + +/* ICON - TALK */ +.icon.talk { + transform: scale(var(--icon-scale, 1)); +} + +.icon.talk, +.icon.talk::after { + box-sizing: border-box; + position: relative; + display: block; + width: 20px; + height: 20px; + border-radius: 100px; + border: 2px dotted currentColor; +} + +.icon.talk::after { + content: ""; + position: absolute; + width: 8px; + height: 8px; + border: 1px solid transparent; + top: 4px; + left: 4px; + box-shadow: + 0 0 0 2px, + inset 0 0 0 2px currentColor; +} + +/* ICON - TRASH */ +.icon.trash { + box-sizing: border-box; + position: relative; + display: block; + transform: scale(var(--icon-scale, 1)); + width: 10px; + height: 12px; + border: 2px solid transparent; + box-shadow: + 0 0 0 2px, + inset -2px 0 0, + inset 2px 0 0; + border-bottom-left-radius: 1px; + border-bottom-right-radius: 1px; + margin-top: 4px; +} + +.icon.trash::after, +.icon.trash::before { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; +} + +.icon.trash::after { + background: currentColor; + border-radius: 3px; + width: 16px; + height: 2px; + top: -4px; + left: -5px; +} + +.icon.trash::before { + width: 10px; + height: 4px; + border: 2px solid; + border-bottom: transparent; + border-top-left-radius: 2px; + border-top-right-radius: 2px; + top: -7px; + left: -2px; +} + +/* ICON - USER */ +.icon.user, +.icon.user::after, +.icon.user::before { + display: block; + box-sizing: border-box; + border: 2px solid; + border-radius: 100px; +} + +.icon.user { + overflow: hidden; + transform: scale(var(--icon-scale, 1)); + width: 22px; + height: 22px; + position: relative; +} + +.icon.user::after, +.icon.user::before { + content: ""; + position: absolute; + top: 2px; + left: 5px; + width: 8px; + height: 8px; +} + +.icon.user::after { + border-radius: 200px; + top: 11px; + left: 0px; + width: 18px; + height: 18px; +} + +/* ICON - WORK */ +.icon.work { + box-sizing: border-box; + position: relative; + display: block; + transform: scale(var(--icon-scale, 1)); + width: 20px; + height: 20px; + border: 2px solid; + border-radius: 22px; +} + +.icon.work::after, +.icon.work::before { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; +} + +.icon.work::before { + width: 12px; + height: 6px; + border: 2px solid; + border-top-left-radius: 100px; + border-top-right-radius: 100px; + top: 2px; + left: 2px; + border-bottom: 0; +} + +.icon.work::after { + width: 18px; + height: 2px; + background: currentColor; + left: -1px; + top: 8px; +} + +/* ICONS - RIGHT/LEFT */ +.icon.right { + box-sizing: border-box; + position: relative; + display: block; + transform: scale(var(--icon-scale, 1)); + width: 22px; + height: 22px; +} + +.icon.right::after, +.icon.right::before { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; + width: 8px; + height: 8px; + border-right: 2px solid; + border-top: 2px solid; + transform: rotate(45deg); + top: 7px; + right: 6px; +} + +.icon.right::after { + right: 11px; +} + +.icon.left { + box-sizing: border-box; + position: relative; + display: block; + transform: scale(var(--icon-scale, 1)); + width: 22px; + height: 22px; +} + +.icon.left::after, +.icon.left::before { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; + width: 8px; + height: 8px; + border-left: 2px solid; + border-bottom: 2px solid; + transform: rotate(45deg); + top: 7px; + left: 6px; +} + +.icon.left::after { + left: 11px; +} + +.icon.map-pin { + box-sizing: border-box; + position: relative; + display: block; + transform: rotate(45deg) scale(var(--icon-scale, 1)); + width: 18px; + height: 18px; + border-radius: 100% 100% 0 100%; + border: 2px solid; + margin-top: -4px; +} + +.icon.map-pin::before { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; + width: 8px; + height: 8px; + border: 2px solid; + top: 3px; + left: 3px; + border-radius: 40px; +} + + +/* AUDIO PLAYER ICONS */ +.icon.skip-back { + box-sizing: border-box; + position: relative; + display: block; + transform: scale(var(--icon-scale, 1)); + width: 22px; + height: 22px; + border: 2px solid; + border-radius: 4px; +} + +.icon.skip-back::after, +.icon.skip-back::before { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; + height: 8px; + top: 5px; +} + +.icon.skip-back::before { + width: 2px; + border-radius: 2px; + right: 11px; + background: currentColor; +} + +.icon.skip-back::after { + width: 0; + border-top: 4px solid transparent; + border-bottom: 4px solid transparent; + border-right: 5px solid; + right: 5px; +} + +.icon.rewind { + box-sizing: border-box; + position: relative; + display: block; + transform: scale(var(--icon-scale, 1)); + border: 2px solid; + border-radius: 4px; + width: 22px; + height: 22px; +} + +.icon.rewind::after, +.icon.rewind::before { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; + width: 6px; + height: 6px; + border-left: 2px solid; + border-bottom: 2px solid; + transform: rotate(45deg); + top: 6px; + left: 5px; +} + +.icon.rewind::after { + left: 9px; +} + +.icon.play { + box-sizing: border-box; + position: relative; + display: block; + transform: scale(var(--icon-scale, 1)); + width: 22px; + height: 22px; + border: 2px solid; + border-radius: 4px; +} + +.icon.play::before { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; + width: 0; + height: 10px; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-left: 6px solid; + top: 4px; + left: 7px; +} + +.icon.pause { + box-sizing: border-box; + position: relative; + display: block; + transform: scale(var(--icon-scale, 1)); + width: 22px; + height: 22px; + border: 2px solid; + border-radius: 4px; +} + +.icon.pause::before { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; + width: 6px; + height: 6px; + left: 6px; + top: 6px; + border-left: 2px solid; + border-right: 2px solid; +} + +.icon.fastforward { + box-sizing: border-box; + position: relative; + display: block; + transform: scale(var(--icon-scale, 1)); + border: 2px solid; + border-radius: 4px; + width: 22px; + height: 22px; +} + +.icon.fastforward::after, +.icon.fastforward::before { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; + width: 6px; + height: 6px; + border-right: 2px solid; + border-top: 2px solid; + transform: rotate(45deg); + top: 6px; + right: 5px; +} + +.icon.fastforward::after { + right: 9px; +} + +.icon.skip-forward { + box-sizing: border-box; + position: relative; + display: block; + transform: scale(var(--icon-scale, 1)); + width: 22px; + height: 22px; + border: 2px solid; + border-radius: 4px; +} + +.icon.skip-forward::after, +.icon.skip-forward::before { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; + height: 8px; + top: 5px; +} + +.icon.skip-forward::before { + width: 2px; + border-radius: 2px; + left: 11px; + background: currentColor; +} + +.icon.skip-forward::after { + width: 0; + border-top: 4px solid transparent; + border-bottom: 4px solid transparent; + border-left: 5px solid; + left: 5px; +} diff --git a/public/index.html b/public/index.html index 2b5a262..09d55ed 100644 --- a/public/index.html +++ b/public/index.html @@ -10,6 +10,7 @@ + diff --git a/public/js/embeds/vimeo.js b/public/js/embeds/vimeo.js index 353b0a0..c5f6215 100644 --- a/public/js/embeds/vimeo.js +++ b/public/js/embeds/vimeo.js @@ -27,7 +27,7 @@ function embed_vimeo(link_info) { diff --git a/public/js/embeds/youtube.js b/public/js/embeds/youtube.js index 1269d6c..5d65b6d 100644 --- a/public/js/embeds/youtube.js +++ b/public/js/embeds/youtube.js @@ -29,7 +29,7 @@ function embed_youtube(link_info) {