Compare commits

...
32 Commits
Author SHA1 Message Date
thePR0M3TH3AN b85a101749 update 2025-10-21 12:51:01 -04:00
thePR0M3TH3AN b33f7f5f23 Merge pull request #756 from PR0M3TH3AN/codex/implement-nip-46-authentication-provider
Add NIP-46 remote signer auth provider
2025-10-21 12:28:43 -04:00
thePR0M3TH3AN 5328168e99 Add NIP-46 remote signer auth provider 2025-10-21 12:25:41 -04:00
thePR0M3TH3AN 73a8d42174 Merge pull request #755 from PR0M3TH3AN/codex/implement-nsec-auth-provider
Add manual nsec authentication provider
2025-10-21 11:55:30 -04:00
thePR0M3TH3AN a6b889fc0d Add nsec manual key authentication provider 2025-10-21 11:55:06 -04:00
thePR0M3TH3AN 44f450a90c Merge pull request #754 from PR0M3TH3AN/codex/refactor-nip-07-integration-and-apis
Refactor Nostr active signer integration for NIP-07 provider
2025-10-21 11:31:14 -04:00
thePR0M3TH3AN fbe6f4a649 Add active signer support and NIP-07 provider refactor 2025-10-21 11:30:28 -04:00
thePR0M3TH3AN edc47631bb Merge pull request #753 from PR0M3TH3AN/codex/refactor-login-modal-and-controller-setup
Refactor login modal to provider-driven controller
2025-10-21 10:36:36 -04:00
thePR0M3TH3AN 1353254445 Refactor login modal to provider-driven controller 2025-10-21 10:36:05 -04:00
thePR0M3TH3AN 97b6e9c11d Merge pull request #752 from PR0M3TH3AN/codex/update-authtype-handling-in-cache
Handle flexible auth provider IDs in cache
2025-10-21 10:02:50 -04:00
thePR0M3TH3AN 3d1486ef79 Handle flexible auth provider IDs in cache 2025-10-21 09:59:46 -04:00
thePR0M3TH3AN 147e57845b Merge pull request #751 from PR0M3TH3AN/codex/create-authproviders-and-update-authservice
Add pluggable auth provider registry
2025-10-21 09:47:38 -04:00
thePR0M3TH3AN 494b00b926 Refactor auth login providers 2025-10-21 09:46:19 -04:00
thePR0M3TH3AN 8f316f915b Merge pull request #397 from PR0M3TH3AN/unstable
Unstable
2025-10-09 21:36:01 -04:00
thePR0M3TH3AN e77db9d07d Merge pull request #263 from PR0M3TH3AN/unstable
Unstable
2025-10-02 21:27:33 -04:00
thePR0M3TH3AN cdff3efe23 Merge pull request #191 from PR0M3TH3AN/unstable
Added watch list.
2025-09-30 22:12:37 -04:00
thePR0M3TH3AN 1d1655d2a7 Merge pull request #166 from PR0M3TH3AN/unstable
Unstable
2025-09-30 09:10:39 -04:00
thePR0M3TH3AN 79e5267524 Merge pull request #156 from PR0M3TH3AN/unstable
Unstable
2025-09-29 13:49:42 -04:00
thePR0M3TH3AN 97c4ccff50 Merge pull request #112 from PR0M3TH3AN/unstable
Unstable
2025-09-25 20:41:49 -04:00
thePR0M3TH3AN fb194f7144 Merge pull request #14 from PR0M3TH3AN/unstable
updated guide
2025-02-15 15:40:30 -05:00
thePR0M3TH3AN b47bd36278 Merge pull request #13 from PR0M3TH3AN/unstable
added profile/channel pages, subscription lists and channel subscription feed, removed "private" video flags
2025-02-10 08:10:37 -05:00
thePR0M3TH3AN 1066c6c74a Merge pull request #12 from PR0M3TH3AN/unstable
increased nostr profile info fetch speed and reliability
2025-02-09 09:45:59 -05:00
thePR0M3TH3AN 5faf572040 Merge pull request #11 from PR0M3TH3AN/unstable
added whitelist filter to video list
2025-02-08 12:06:28 -05:00
thePR0M3TH3AN 40be8da9aa Merge pull request #10 from PR0M3TH3AN/unstable
updated site text and beacon
2025-02-07 17:33:35 -05:00
thePR0M3TH3AN 3001e956c1 Merge pull request #9 from PR0M3TH3AN/unstable
Added sidebar and massive performance improvements
2025-02-07 13:12:22 -05:00
thePR0M3TH3AN c4ee73d8c0 Merge pull request #8 from PR0M3TH3AN/unstable
added block filter for link path to blacklisted npub content
2025-02-04 12:20:01 -05:00
thePR0M3TH3AN 1c486e2799 Merge pull request #7 from PR0M3TH3AN/unstable
overall much better.
2025-02-04 11:38:40 -05:00
thePR0M3TH3AN 587f2b5f96 Merge pull request #6 from PR0M3TH3AN/unstable
fixed bug when editing older videos without the needed fields
2025-02-03 14:20:38 -05:00
thePR0M3TH3AN 87e5ccdee5 Merge pull request #5 from PR0M3TH3AN/unstable
Added custom Nostr DM form submissions for: Whitelisting, Bug Reporting, Feature Requesting and Content Appeals.
2025-02-03 09:39:16 -05:00
thePR0M3TH3AN 77a4d53e54 Merge pull request #4 from PR0M3TH3AN/unstable
Added better UI modal flow and reliable revert/edit/delete functionality
2025-02-01 21:34:45 -05:00
thePR0M3TH3AN 518428fe99 Merge pull request #3 from PR0M3TH3AN/unstable
adjusted footer, added community guidelines and created banner for forms
2025-01-30 15:41:54 -05:00
thePR0M3TH3AN 65ff3ba841 Merge pull request #2 from PR0M3TH3AN/unstable
Made a ton of changes. IDK. seems to work well.
2025-01-30 09:04:50 -05:00
277 changed files with 53836 additions and 8652 deletions

No files matched your search

+81
View File
@@ -0,0 +1,81 @@
name: CI
on:
push:
branches:
- main
pull_request:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20]
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Cache npm directory
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ matrix.node-version }}-npm-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-${{ matrix.node-version }}-npm-
- name: Cache node_modules
id: node-modules-cache
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-${{ matrix.node-version }}-modules-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-${{ matrix.node-version }}-modules-
- name: Install dependencies
if: steps.node-modules-cache.outputs.cache-hit != 'true'
run: npm ci
- name: Install ripgrep
run: sudo apt-get update && sudo apt-get install -y ripgrep
- name: Run lint suite
run: npm run lint
- name: Build CSS
run: npm run build:css
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Run visual regression tests
id: visual
run: npm run test:visual
continue-on-error: true
- name: Upload Playwright artifacts
if: steps.visual.outcome == 'failure'
uses: actions/upload-artifact@v4
with:
name: playwright-visual-diffs
path: |
playwright-report
test-results
if-no-files-found: ignore
- name: Run unit tests
run: npm run test:unit
- name: Check CSS artifacts are clean
run: npm run check:css
- name: Fail if visual regressions detected
if: steps.visual.outcome == 'failure'
run: exit 1
+3
View File
@@ -2,3 +2,6 @@ saved_config.yaml
repo-context.txt
node_modules/
src/webtorrent-docs/
playwright-report/
test-results/
tests/visual/__snapshots__/
+23 -5
View File
@@ -1,6 +1,6 @@
# Bitvid — AI Agent Guide
# bitvid — AI Agent Guide
This guide tells AI agents how to keep Bitvid aligned with the current product direction. Follow it whenever you touch code, content, or documentation inside this repository.
This guide tells AI agents how to keep bitvid aligned with the current product direction. Follow it whenever you touch code, content, or documentation inside this repository.
---
@@ -10,13 +10,31 @@ This guide tells AI agents how to keep Bitvid aligned with the current product d
* **Unstable** is our experimentation lane. Gate risky behavior behind feature flags defined in `js/constants.js` and document the toggle/rollback plan in PR descriptions.
* **Emergency response:** If a change regresses URL-first playback or breaks magnet parsing, revert immediately, call this out in the PR, and annotate the AGENTS.md changelog with remediation tips. **Note:** These regressions usually surface in dev logs and tests around playback/magnet helpers—reference them when triaging so future agents don’t repeat the mistake.
### Browser logging policy
* Route every browser log through `js/utils/logger.js`. Do **not** call `console.*` directly.
* Use `logger.dev.*` for diagnostics that should disappear in production (flagged by `IS_DEV_MODE`/`isDevMode`). Use `logger.user.*` for operator-facing warnings and errors that must reach production consoles.
* Before promoting a build, flip `IS_DEV_MODE` in `config/instance-config.js` to match the target environment so `window.__BITVID_DEV_MODE__` and the logger channels behave correctly.
* Review and follow [`docs/logging.md`](docs/logging.md) whenever you add new logging or touch inline scripts.
## 2. Mission: URL‑First Playback with WebTorrent Fallback
* **Goal:** Always deliver smooth playback while keeping hosting costs low.
* **Primary transport:** A hosted video URL (MP4/WebM/HLS/DASH) that the `<video>` element can stream directly.
* **Fallback transport:** A WebTorrent magnet that includes browser‑safe trackers plus optional HTTP hints.
* **Runtime behavior:** Call `bitvidApp.playVideoWithFallback({ url, magnet })` in `js/app.js`. This function orchestrates URL probing and magnet fallback, delegating low-level stream handling to `js/services/playbackService.js`. `js/playbackUtils.js` now provides magnet/session helper utilities rather than the playback entry point itself.
* **Content contract:** Bitvid posts (Nostr kind `30078`) must include a `title` and at least one of `url` or `magnet`. Prefer to publish both along with optional `thumbnail`, `description`, and `mode` fields. When mirroring to NIP‑94 (`kind 1063`), copy the hosted URL and (optionally) the magnet so other clients can discover the same asset.
* **Content contract:** bitvid posts (Nostr kind `30078`) must include a `title` and at least one of `url` or `magnet`. Prefer to publish both along with optional `thumbnail`, `description`, and `mode` fields. When mirroring to NIP‑94 (`kind 1063`), copy the hosted URL and (optionally) the magnet so other clients can discover the same asset.
## Styling & Theming Rules (token-first)
* **Single source of truth:** All colors, spacing, radii, and typography decisions must flow through our design tokens; never reach for ad-hoc HEX/RGB values when a token exists.
* **Theme scopes:** Tokens live at three levels—`core` (global primitives), `theme` (light/dark surface/background mappings), and `component` (contextual overrides). Additions must document their scope and consumers.
* **Semantic tokens:** Maintain this canonical list: `bg`, `bg-muted`, `bg-raised`, `text`, `text-muted`, `border`, `border-strong`, `accent`, `accent-hover`, `accent-active`, `accent-muted`, `accent-contrast`, `danger`, `warning`, `success`, `info`, `overlay`, `overlay-strong`.
* **Palette guidance:** Map semantic tokens to palette primitives defined in the CSS build pipeline; when a new primitive is needed, extend the palette once and remap consumers rather than hard-coding colors downstream.
* **Icon contrast rule:** Icons and glyph-only buttons must meet a minimum 3:1 contrast ratio against their background; prefer `accent-contrast` or `text` tokens when in doubt.
* **Accessibility expectations:** Default themes must satisfy WCAG 2.1 AA contrast ratios for text and controls, and preserve focus outlines sourced from the token set.
* **Prohibitions:** Do not commit inline styles, `<font>` tags, or CSS variables whose values are literal colors; always reference the appropriate token variables.
* **Theme toggling:** Implement new themes through token swaps only (e.g., toggling the `data-theme` attribute) and keep component logic agnostic to specific palette values. Document toggles alongside the [README CSS build pipeline](README.md#css-build-pipeline).
---
@@ -70,9 +88,9 @@ Document the run in PR descriptions so QA can cross-reference results.
---
## 6. Moderation Quickstart (Bitvid)
## 6. Moderation Quickstart (bitvid)
The moderation and admin list tooling remain active in Bitvid and must follow Nostr moderation policies and QA steps. Review `docs/moderation/README.md` and these key expectations:
The moderation and admin list tooling remain active in bitvid and must follow Nostr moderation policies and QA steps. Review `docs/moderation/README.md` and these key expectations:
1. **Report thresholds:** Parse NIP-56 reports and compute `trustedReportCount` using **only F1 followers**.
2. **Default behaviors:** Blur thumbnails at ≥3 F1 `nudity` reports and block autoplay at ≥2.
+40
View File
@@ -0,0 +1,40 @@
# Contributing to bitvid
Thanks for helping build an open video ecosystem on nostr!
## License Model
- **App** (this repo): GPL-3.0-or-later
- **Potential SDKs/embeds** (if split later): Apache-2.0 (to keep integration friction low)
All contributions to this repository are made under the repository’s license (GPL-3.0-or-later).
## Developer Certificate of Origin (DCO)
We use the DCO to keep contributions simple and legally sound. Sign your commits with `-s`:
```bash
git commit -s -m "feat: add cool thing"
```
This adds a `Signed-off-by: Your Name <[email protected]>` line and certifies you have the right to contribute under the project license.
See [https://developercertificate.org/](https://developercertificate.org/) for the full text.
## (Optional) Contributor License Agreement (CLA)
If we later introduce **dual-licensing** options (e.g., commercial licenses for enterprises), we may add an *optional* CLA for contributors who want to grant relicense rights. You will be asked to agree explicitly in any PR that requires it. We will keep community use under GPL-3.0-or-later.
## Code Guidelines
* Prefer small, focused PRs.
* Add tests where reasonable; keep UI accessible.
* Follow existing formatting/lint rules.
* Update docs and user-facing text when behavior changes.
## Security
If you find a vulnerability, please email **[[email protected]](mailto:[email protected])**. We’ll coordinate a responsible disclosure window before public release.
## Trademark
The **bitvid** name and logos are trademarks (see `TRADEMARKS.md`). Don’t use them in ways that imply official sponsorship or confuse users. “Powered by bitvid” attribution is encouraged.
+693
View File
@@ -0,0 +1,693 @@
bitvid — decentralized, client-side video app for the nostr ecosystem
Copyright (c) 2025 Adam Malin
SPDX-License-Identifier: GPL-3.0-or-later
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
this program. If not, see <https://www.gnu.org/licenses/>.
----------------------------------------------------------------------
PASTE THE FULL OFFICIAL GPLv3 LICENSE TEXT BELOW THIS LINE
(Use the canonical text from https://www.gnu.org/licenses/gpl-3.0.txt)
----------------------------------------------------------------------
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work for
making modifications to it. "Object code" means any non-source form of
a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users can
regenerate automatically from other parts of the Corresponding Source.
The Corresponding Source for a work in source code form is that same
work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not convey,
without conditions so long as your license otherwise remains in force.
You may convey covered works to others for the sole purpose of having
them make modifications exclusively for you, or provide you with
facilities for running those works, provided that you comply with the
terms of this License in conveying all material for which you do not
control copyright. Those thus making or running the covered works for
you must do so exclusively on your behalf, under your direction and
control, on terms that prohibit them from making any copies of your
copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under the
conditions stated below. Sublicensing is not allowed; section 10 makes
it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article 11
of the WIPO copyright treaty adopted on 20 December 1996, or similar
laws prohibiting or restricting circumvention of such measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms of
sections 4 and 5, provided that you also convey the machine-readable
Corresponding Source under the terms of this License, in one of these
ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms of
that license document, provided that the further restriction does not
survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the additional
terms that apply to those files, or a notice indicating where to find
the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions; the above
requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or run a
copy of the Program. Ancillary propagation of a covered work occurring
solely as a consequence of using peer-to-peer transmission to receive a
copy likewise does not require acceptance. However, nothing other than
this License grants you permission to propagate or modify any covered
work. These actions infringe copyright if you do not accept this
License. Therefore, by modifying or propagating a covered work, you
indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone to
copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is in
the business of distributing software, under which you make payment to
the third party based on the extent of your activity of conveying the
work, and under which the third party grants, to any of the parties who
would receive the covered work from you, a discriminatory patent
license (a) in connection with copies of the covered work conveyed by
you (or copies made from those copies), or (b) primarily for and in
connection with specific products or compilations that contain the
covered work, unless you entered into that arrangement, or that patent
license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future versions
of the GNU General Public License can be used, that proxy's public
statement of acceptance of a version permanently authorizes you to choose
that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
bitvid Copyright (C) 2025 Adam Malin
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w`.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c` for details.
The hypothetical commands `show w` and `show c` should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+173 -28
View File
@@ -11,7 +11,11 @@
## Features
- **Decentralized Sharing**: Video sharing without central servers.
- **Private Video Listings**: Hide cards from shared grids so only the owner sees them.
- **Cloudflare R2 Uploads**: Publish directly from the Upload modal's Cloudflare flow with progress tracking and credential helpers.
- **Encrypted Watch History**: Sync viewing activity privately through the NIP-04 encrypted pipeline with local fallbacks.
- **Live View Counters**: Subscribe to view events and see totals update in real time on video cards and the video modal.
- **Lightning Zaps**: Tip creators with Lightning payments via the Zap controls in the video modal.
- **Private Video Listings**: Hide cards from shared grids by flipping the visibility switch in the [Edit Video modal](components/edit-video-modal.html) after publishing.
- **Nostr Integration**: Use Nostr keys for login and interaction.
- **WebTorrent Streaming**: Stream videos directly through torrent technology.
- **Developer-Friendly**: Open source and customizable for your needs.
@@ -27,25 +31,51 @@
2. **Login with Nostr**:
- Use a compatible Nostr browser extension or manually input your public key.
### Post a video
### Upload a video
The upload modal enforces **Title + (URL or Magnet)**. Hosted URLs are strongly recommended so the player can start instantly, and you may also add a magnet for resilience.
Open the **Upload** modal from the header toolbar and start by selecting the NIP-71 event type that matches your post. The chooser sets the correct Nostr kind and schema defaults so the published note follows [`docs/nostr-event-schemas.md`](docs/nostr-event-schemas.md).
- **Title**: Required. Matches the in-app validation copy.
- **Hosted URL (recommended)**: Supply an HTTPS MP4/WebM/HLS/DASH asset. This is the primary playback path.
- **Magnet (optional but encouraged)**: Paste the literal `magnet:?xt=urn:btih:...` string. The form decodes it with `safeDecodeMagnet()` to prevent hash corruption. Never wrap magnets in `new URL()`—store them raw or decode then pass directly to the helpers.
- **Web seeds (`ws=`)**: HTTPS only. Point to a file root (e.g., `https://cdn.example.com/video/`). Mixed-content (`http://`) hints are rejected just like the modal message explains.
- **Additional sources (`xs=`)**: Recommend adding an HTTPS `.torrent` link so WebTorrent peers can bootstrap faster.
- **Trackers**: Bitvid’s browser client only connects to WSS trackers shipped in `js/constants.js`. Do not add UDP or plaintext HTTP trackers to published magnets.
- **Private toggle**: Hides the card from shared grids so only you see it and adds a purple accent so the private state stands out.
Both upload modes expose metadata repeaters for **variants**, **captions/text tracks**, **segments**, **hashtags**, **participants**, and **references**. Use these when you have multiple playback qualities (variants), accessibility tracks (captions), multipart drops (segments), discoverability tags (hashtags), credited collaborators (participants), or cross-posts/threads (references). Skip any repeater you don’t need—the base schema stays valid without them.
Pick the flow that matches your source material:
- **Custom (hosted URL or magnet)**: Provide a title plus an HTTPS video URL and/or a WebTorrent magnet. The form requires at least one transport, validates `ws=`/`xs=` hints, keeps magnets raw by decoding them with `safeDecodeMagnet()` before publish, and applies whatever metadata repeaters you configured.
- **Cloudflare (R2 direct upload)**: Enter your Cloudflare credentials in the guided form, optionally expand the **Advanced settings** accordion to override pathing or access controls, then drop media files for bitvid to upload through the R2 API. The modal tracks progress, applies your metadata selections, auto-fills the primary `imeta` variant once the upload completes, and publishes the resulting R2 URL back into the note automatically.
Hosted URLs remain the preferred playback path, and you can still add a magnet or supplemental web seeds when using either mode. Use the **Private** toggle to keep the resulting card visible only to you, and lean on the repeaters whenever you want to surface richer context or alternate assets as outlined in the event schema reference.
### How playback works
1. **URL-first**: `playVideoWithFallback({ url, magnet })` attempts the hosted URL immediately. Healthy URLs deliver the full experience without touching P2P resources.
2. **WebTorrent fallback**: If the URL probe fails or returns an error status, Bitvid falls back to WebTorrent using the raw magnet. The helpers append HTTPS `ws=`/`xs=` hints so peers seed quickly.
2. **WebTorrent fallback**: If the URL probe fails or returns an error status, bitvid falls back to WebTorrent using the raw magnet. The helpers append HTTPS `ws=`/`xs=` hints so peers seed quickly.
3. **Safety checks**: Magnets are decoded with `safeDecodeMagnet()` and normalized via `normalizeAndAugmentMagnet()` before reaching WebTorrent. Trackers remain WSS-only to satisfy browser constraints.
4. **Operator playbook**: If a deployment causes playback regressions, flip the relevant feature flags back to their default values in `js/constants.js` and redeploy. Capture the rollback steps in AGENTS.md and the PR description so the Main channel stays stable.
### Watch history & view counts
- **Encrypted watch history**: When you opt into watch history, bitvid stores entries locally and syncs them as NIP-04 encrypted events so only your keys can decrypt them. The History view and the Profile modal’s History tab hydrate from relays when available and gracefully fall back to the local cache when offline.
- **Live view counters**: The `viewCounter` module hydrates totals from relays, subscribes to live updates, and dedupes local plays. Video cards and the video modal update in real time as new view events arrive.
### Support creators with Lightning
Click a card to open the video modal and use the **Zap** button (lightning bolt icon) to send Lightning payments. The Zap dialog walks you through selecting an amount, splitting sats, and pushing the payment through your active wallet connection or Nostr Wallet Connect session.
### Moderation & safety controls
Operators can tune thresholds and lists from the Profile modal:
- The **Moderation** tab manages blur and autoplay limits plus the relay-synced mute/ban lists.
- Each video card’s **More** menu surfaces per-video actions powered by `videoMenuRenderers.js`, including:
- **Repost** to boost the note as a kind 6 event.
- **Mirror** to republish the hosted URL and magnet as a kind 1063 event.
- **Rebroadcast** to push the existing event to additional relays (see cooldown note below).
- **Remove from history** to clear the item from your encrypted watch log.
- **Mute/Unmute creator** to control local visibility without severing follows.
- **Blacklist creator** for operators who maintain shared deny lists.
- **Block creator** to drop their events entirely.
- **Report** to issue a NIP-56 moderation report.
- When a rebroadcast hits rate limits, the app surfaces cooldown guidance (e.g., “Rebroadcast is cooling down. Try again in 30s.”) so operators know when to retry without spamming relays.
---
## For Developers
@@ -83,19 +113,112 @@ Tailwind utilities are generated from `css/tailwind.source.css` and themed via
the shared tokens in `css/tokens.css`. Install dependencies once and lean on the
package scripts to keep formatting, linting, and generated output consistent:
- **Token imports:** `css/tokens.css` feeds `css/style.css`, which in turn seeds
`tailwind.config.cjs` so utilities inherit the same palette, spacing, and
typography primitives across the stack.
- **Core scripts:**
```bash
npm run format # normalize CSS/HTML/JS/MD sources with Prettier + tailwindcss plugin
npm run lint:css # enforce design token usage and guard against raw hex colors
npm run build:css # rebuild css/tailwind.generated.css from tailwind.source.css
npm run check:css # fail CI if tailwind.generated.css is out of date
```
- **No hard-coded colors:** Follow the token-first rules in `AGENTS.md`—reach
for semantic tokens instead of literal HEX/RGB values.
- **Theme scopes:** Apply tokens by toggling the `data-theme` attribute on
scope wrappers so components stay palette-agnostic.
```bash
npm install # install Prettier, Stylelint, and Tailwind toolchain
npm run format # format CSS/HTML/JS/MD with Prettier + tailwindcss plugin
npm run lint:css # enforce token usage and forbid raw hex colors
npm run build:css # rebuild css/tailwind.generated.css from tailwind.source.css
npm run check:css # CI-friendly guard that fails if tailwind.generated.css is dirty
npm install # install Prettier, Stylelint, and Tailwind toolchain
npm run format # format CSS/HTML/JS/MD with Prettier + tailwindcss plugin
npm run lint # run CSS, hex color, and inline-style guards in one pass
npm run lint:css # enforce token usage and forbid raw hex colors
npm run lint:inline-styles # fail CI if inline style attributes or element.style usage slip in
npm run build:css # rebuild css/tailwind.generated.css from tailwind.source.css
npm run build:beacon # bundle torrent/dist assets and re-run the inline-style guard
npm run build:beacon:bundle # bundle beacon assets without running the guard (rarely needed)
npm run check:css # CI-friendly guard that fails if tailwind.generated.css is dirty
```
#### Admin runbook: regenerate Tailwind styles
Need to refresh `css/tailwind.generated.css` after token or template changes?
Run these commands from the repo root:
1. Install dependencies (only required after cloning or when packages change):
```bash
npm install
```
2. Rebuild the Tailwind bundle:
```bash
npm run build:css
```
3. Optional: confirm the generated file is committed and up to date.
```bash
npm run check:css
```
4. Commit the updated `css/tailwind.generated.css` alongside any HTML/JS edits
that rely on the new utilities so deploys pick up the refreshed styles.
Inline styles are intentionally blocked. `npm run lint:inline-styles` scans HTML
and scripts for `style=` attributes, `element.style`, or `style.cssText` usage
and will fail CI until offending markup is moved into the shared CSS/token
system.
#### Design token guard
`npm run lint:tokens` inspects JavaScript **and** `css/tailwind.source.css`
for raw `px`/`rem` measurements. Prefer design tokens, Tailwind `theme()` calls,
or existing utilities over hard-coded lengths. When unavoidable (for example
hairline borders or browser reset quirks) add an explicit allowlist entry in
`scripts/check-design-tokens.mjs` so future contributors understand why the
constant exists.
The script now crawls every markup-emitting module under `js/ui`,
`js/channelProfile.js`, and `torrent/ui`, so UI tweaks anywhere in the repo
benefit from the guard without manual configuration.
Beacon builds inherit the same rule. `npm run build:beacon` now bundles
`torrent/dist` and immediately re-runs the inline-style checker against the
fresh output so third-party dependencies cannot sneak inline style mutations
into production. Use `npm run build:beacon:bundle` only if you need the raw
esbuild output for debugging.
The build command compiles Tailwind with `tailwind.config.cjs`, runs it through
the PostCSS pipeline defined in `postcss.config.cjs` (for autoprefixing), and
emits the purged, minified stylesheet at `css/tailwind.generated.css`. Commit the
regenerated file alongside any template changes so deployments pick up the
latest styles.
latest styles. This generated bundle is the only stylesheet we ship—reference
`css/tailwind.generated.css` everywhere and avoid vendoring older `tailwind.min.css`
artifacts.
### Logo usage
- Wrap inline logo `<svg>` elements with the `.bv-logo` component class. It
seeds `--logo-wordmark-color`, `--logo-accent-color`, and
`--logo-background-color` with the palette tokens exported in
`css/tokens.css`.
- Adjust variants through data attributes instead of inline styles. For
example, `data-wordmark="current"` ties the wordmark to the wrapper’s
`currentColor`, `[data-variant="inverse"]` swaps in the inverse palette, and
`data-accent="current"` makes the accent follow the surrounding text when
needed.
- Inside the SVG, target elements with `.bv-logo__wordmark`,
`.bv-logo__accent`, and `.bv-logo__background` so fills inherit the custom
properties seeded by `.bv-logo`. Group paths with `<g>` wrappers where
possible to avoid repeating classes on every primitive.
- Use Tailwind text utilities (such as `text-text-strong` or `text-text`) on the
wrapper to resolve to token-backed colors. Avoid hard-coded hex colors in the
SVG; customize the logo via the provided classes when deployments need a
different accent or background.
### Configuration
@@ -103,15 +226,31 @@ latest styles.
- Central place for instance-specific values like the Super Admin npub and the
default whitelist-only mode setting. Update the documented exports here when
preparing a new deployment.
- Flip `IS_DEV_MODE` to `false` before shipping production builds. The flag
flows into `js/config.js` as `isDevMode`, seeds the
`window.__BITVID_DEV_MODE__` global for inline scripts, and gates whether the
dev logging channel emits to the console. See
[`docs/logging.md`](docs/logging.md) for rollout guidance.
- Tune `PLATFORM_FEE_PERCENT` (0–100) to keep a percentage of Lightning tips.
When the fee is positive, BitVid routes the platform’s split to
When the fee is positive, bitvid routes the platform’s split to
`PLATFORM_LUD16_OVERRIDE`, so set it to the Lightning address that should
receive the sats (or publish a `lud16` on the Super Admin profile). Leave
the fee at `0` to pass through every satoshi.
- Populate `DEFAULT_RELAY_URLS_OVERRIDE` with WSS URLs to replace the bundled
relay bootstrap list. Keep it empty to stick with the upstream defaults.
- **`config.js`**:
- Toggle `isDevMode` for development (`true`) or production (`false`).
- Customize `THEME_ACCENT_OVERRIDES` with `#RRGGBB` hex strings when you want
light or dark mode to use different accent, accent-strong, or
accent-pressed colors. Leave the values `null` to inherit the defaults from
`css/tokens.css`.
- **`js/config.js`**:
- Re-exports `isDevMode` (derived from `IS_DEV_MODE`) for modules, publishes
`window.__BITVID_DEV_MODE__`, and centralizes the global configuration
surface.
- **`js/utils/logger.js`**:
- Provides the shared `logger` utility. Route user-facing errors through
`logger.user` and keep experimental diagnostics on `logger.dev` so operators
can quiet development noise in production. Details live in
[`docs/logging.md`](docs/logging.md).
- **`js/constants.js`**:
- Source for browser-safe tracker lists and feature flags that govern WebTorrent behavior.
- **Magnet helpers**:
@@ -119,7 +258,7 @@ latest styles.
### Relay compatibility
Bitvid now requests per-video discussion counts using the NIP-45 `COUNT` frame. The bundled client opens each relay via
bitvid now requests per-video discussion counts using the NIP-45 `COUNT` frame. The bundled client opens each relay via
`this.pool.ensureRelay(url)` and streams a raw `COUNT` message, so your relay stack must understand that verb (nostr-tools ≥ 1.8
or any relay advertising NIP-45 support). Relays that do not implement `COUNT` are skipped gracefully—the UI keeps the count
placeholder at “—” and development builds log a warning—so mixed deployments remain usable while you phase in compatible relays.
@@ -163,7 +302,13 @@ placeholder at “—” and development builds log a warning—so mixed deploym
## Testing
Use the manual QA checklist before releases or when altering upload/playback flows:
Continuous integration runs CSS linting/builds, the Playwright kitchen-sink snapshots, and the Node-based unit tests on every push.
Before pushing, run `npm run check:css` locally so `css/tailwind.generated.css` stays clean. Pair that with `npm run test:unit` for
application logic changes and `npm run test:visual` for presentation updates to mirror the CI surface area.
### Manual QA checklist
Use this checklist before releases or when altering upload/playback flows:
1. Open the Upload modal, confirm validation (title plus URL or magnet), and test submissions for URL-only, magnet-only, and combined entries.
2. Publish a post with both URL and magnet, verify the player streams the hosted URL, then simulate a URL failure and confirm WebTorrent playback.
@@ -173,12 +318,6 @@ Use the manual QA checklist before releases or when altering upload/playback flo
See [`docs/qa.md`](docs/qa.md) for the copy/paste-friendly checklist we share with QA.
For automated DOM-focused regression coverage, run the Node-based test suite:
```bash
node tests/profile-modal-controller.test.mjs
```
---
## Acknowledgments
@@ -196,3 +335,9 @@ node tests/profile-modal-controller.test.mjs
- **Website**: [bitvid.network](https://bitvid.network)
- **GitHub**: [PR0M3TH3AN](https://github.com/PR0M3TH3AN)
- **Nostr**: [npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe](https://primal.net/p/npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe)
---
## License
GPL-3.0-or-later. See [LICENSE](./LICENSE).
+21
View File
@@ -0,0 +1,21 @@
# bitvid Trademarks
“bitvid”, the word mark, and any associated logos are trademarks of **Adam Malin**.
## You May
- Accurately describe unmodified builds as “bitvid”.
- Use phrases like “Powered by bitvid”.
- Link to https://bitvid.network.
## You May Not
- Use “bitvid” or confusingly similar names/logos in a way that implies official affiliation, endorsement, or ownership by the bitvid project.
- Launch a service named “bitvid” without permission.
- Use our logos in your product branding without permission.
## Fair Use & Community
We aim to foster a healthy open ecosystem. Nominative and descriptive uses that do not mislead users are generally fine. When in doubt, ask.
**Contact:** trademarks@bitvid.network
+2 -2
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M12,0C5.373,0 0,5.373 0,12C0,18.627 5.373,24 12,24C18.627,24 24,18.627 24,12C24,5.373 18.627,0 12,0ZM9.967,16.01C10.531,14.221 11.599,12.078 11.788,11.536C12.061,10.749 11.577,10.4 10.048,11.745L9.708,11.105C11.452,9.208 15.043,8.779 13.821,11.718C13.058,13.553 12.512,14.792 12.2,15.748C11.745,17.141 12.894,16.576 14.019,15.537C14.172,15.787 14.222,15.868 14.375,16.156C11.877,18.534 9.104,18.744 9.967,16.01ZM14.709,7.841C14.177,8.294 13.389,8.284 12.948,7.819C12.507,7.354 12.581,6.611 13.112,6.158C13.644,5.705 14.432,5.716 14.873,6.18C15.312,6.646 15.24,7.389 14.709,7.841Z" style="fill:rgb(92,111,138);fill-rule:nonzero;"/>
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2">
<path d="M12,0C5.373,0 0,5.373 0,12C0,18.627 5.373,24 12,24C18.627,24 24,18.627 24,12C24,5.373 18.627,0 12,0ZM9.967,16.01C10.531,14.221 11.599,12.078 11.788,11.536C12.061,10.749 11.577,10.4 10.048,11.745L9.708,11.105C11.452,9.208 15.043,8.779 13.821,11.718C13.058,13.553 12.512,14.792 12.2,15.748C11.745,17.141 12.894,16.576 14.019,15.537C14.172,15.787 14.222,15.868 14.375,16.156C11.877,18.534 9.104,18.744 9.967,16.01ZM14.709,7.841C14.177,8.294 13.389,8.284 12.948,7.819C12.507,7.354 12.581,6.611 13.112,6.158C13.644,5.705 14.432,5.716 14.873,6.18C15.312,6.646 15.24,7.389 14.709,7.841Z" fill="#5C6F8A" fill-rule="nonzero"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

+2 -2
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2">
<g transform="matrix(0.0349343,0,0,0.0349343,-55.5414,-28.6416)">
<path d="M1878.62,1496.58C1719.5,1470.41 1597.95,1332.09 1597.95,1165.6C1597.95,980.47 1748.25,830.166 1933.38,830.166C2118.51,830.166 2268.82,980.47 2268.82,1165.6C2268.82,1331.86 2147.6,1470.03 1988.8,1496.47L1988.8,1466.79C2131.38,1440.68 2239.59,1315.68 2239.59,1165.6C2239.59,996.604 2102.38,859.399 1933.38,859.399C1764.39,859.399 1627.18,996.604 1627.18,1165.6C1627.18,1315.91 1735.72,1441.07 1878.62,1466.91L1878.62,1496.58ZM1878.62,1400.68C1771.77,1375.85 1692.06,1279.96 1692.06,1165.6C1692.06,1032.41 1800.19,924.274 1933.38,924.274C2066.57,924.274 2174.71,1032.41 2174.71,1165.6C2174.71,1279.73 2095.32,1375.46 1988.8,1400.53L1988.8,1370.37C2079.02,1345.98 2145.48,1263.49 2145.48,1165.6C2145.48,1048.54 2050.44,953.507 1933.38,953.507C1816.33,953.507 1721.29,1048.54 1721.29,1165.6C1721.29,1263.73 1788.07,1346.38 1878.62,1370.55L1878.62,1400.68ZM1878.62,1304.01C1823.55,1282.18 1784.56,1228.41 1784.56,1165.6C1784.56,1083.46 1851.24,1016.77 1933.38,1016.77C2015.52,1016.77 2082.21,1083.46 2082.21,1165.6C2082.21,1228.16 2043.53,1281.76 1988.8,1303.75L1988.8,1271.59C2026.93,1251.59 2052.98,1211.62 2052.98,1165.6C2052.98,1099.6 1999.39,1046.01 1933.38,1046.01C1867.38,1046.01 1813.79,1099.6 1813.79,1165.6C1813.79,1211.88 1840.13,1252.05 1878.62,1271.93L1878.62,1304.01ZM1933.38,1109.82C1964.17,1109.82 1989.16,1134.82 1989.16,1165.6C1989.16,1196.39 1964.17,1221.38 1933.38,1221.38C1902.6,1221.38 1877.6,1196.39 1877.6,1165.6C1877.6,1134.82 1902.6,1109.82 1933.38,1109.82Z" style="fill:rgb(92,111,138);"/>
<path d="M1878.62,1496.58C1719.5,1470.41 1597.95,1332.09 1597.95,1165.6C1597.95,980.47 1748.25,830.166 1933.38,830.166C2118.51,830.166 2268.82,980.47 2268.82,1165.6C2268.82,1331.86 2147.6,1470.03 1988.8,1496.47L1988.8,1466.79C2131.38,1440.68 2239.59,1315.68 2239.59,1165.6C2239.59,996.604 2102.38,859.399 1933.38,859.399C1764.39,859.399 1627.18,996.604 1627.18,1165.6C1627.18,1315.91 1735.72,1441.07 1878.62,1466.91L1878.62,1496.58ZM1878.62,1400.68C1771.77,1375.85 1692.06,1279.96 1692.06,1165.6C1692.06,1032.41 1800.19,924.274 1933.38,924.274C2066.57,924.274 2174.71,1032.41 2174.71,1165.6C2174.71,1279.73 2095.32,1375.46 1988.8,1400.53L1988.8,1370.37C2079.02,1345.98 2145.48,1263.49 2145.48,1165.6C2145.48,1048.54 2050.44,953.507 1933.38,953.507C1816.33,953.507 1721.29,1048.54 1721.29,1165.6C1721.29,1263.73 1788.07,1346.38 1878.62,1370.55L1878.62,1400.68ZM1878.62,1304.01C1823.55,1282.18 1784.56,1228.41 1784.56,1165.6C1784.56,1083.46 1851.24,1016.77 1933.38,1016.77C2015.52,1016.77 2082.21,1083.46 2082.21,1165.6C2082.21,1228.16 2043.53,1281.76 1988.8,1303.75L1988.8,1271.59C2026.93,1251.59 2052.98,1211.62 2052.98,1165.6C2052.98,1099.6 1999.39,1046.01 1933.38,1046.01C1867.38,1046.01 1813.79,1099.6 1813.79,1165.6C1813.79,1211.88 1840.13,1252.05 1878.62,1271.93L1878.62,1304.01ZM1933.38,1109.82C1964.17,1109.82 1989.16,1134.82 1989.16,1165.6C1989.16,1196.39 1964.17,1221.38 1933.38,1221.38C1902.6,1221.38 1877.6,1196.39 1877.6,1165.6C1877.6,1134.82 1902.6,1109.82 1933.38,1109.82Z" fill="#5C6F8A"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

+2 -2
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2">
<g transform="matrix(1.09255,0,0,1.09255,-0.61419,-0.557138)">
<path d="M11.546,1.337C17.151,1.337 21.702,5.888 21.702,11.493C21.702,17.099 17.151,21.65 11.546,21.65C5.94,21.65 1.389,17.099 1.389,11.493C1.389,5.888 5.94,1.337 11.546,1.337ZM10.288,13.666C10.735,14.248 11.352,14.54 12.138,14.54C12.878,14.54 13.491,14.29 13.975,13.792C14.46,13.293 14.702,12.697 14.702,12.003C14.702,11.141 14.3,10.526 13.495,10.158C13.924,9.994 14.274,9.743 14.544,9.407C14.814,9.071 14.949,8.687 14.949,8.254C14.949,7.757 14.757,7.325 14.375,6.959C13.992,6.593 13.445,6.409 12.734,6.409C11.929,6.409 11.274,6.652 10.768,7.136C10.262,7.62 9.882,8.454 9.628,9.638L8.143,16.578L9.666,16.578L10.288,13.666ZM11.8,9.445L11.532,10.619C11.611,10.616 11.672,10.614 11.715,10.614C12.741,10.614 13.254,11.043 13.254,11.901C13.254,12.398 13.108,12.773 12.817,13.025C12.525,13.277 12.221,13.403 11.902,13.403C11.566,13.403 11.283,13.287 11.052,13.057C10.822,12.826 10.706,12.491 10.706,12.051C10.706,11.819 10.744,11.524 10.819,11.166L11.162,9.595C11.266,9.112 11.373,8.744 11.484,8.49C11.595,8.236 11.76,8.03 11.98,7.871C12.2,7.712 12.449,7.632 12.728,7.632C12.953,7.632 13.137,7.698 13.278,7.831C13.419,7.963 13.49,8.124 13.49,8.313C13.49,8.592 13.372,8.851 13.136,9.091C12.9,9.33 12.53,9.45 12.026,9.45C11.983,9.45 11.908,9.448 11.8,9.445Z" style="fill:rgb(92,111,138);"/>
<path d="M11.546,1.337C17.151,1.337 21.702,5.888 21.702,11.493C21.702,17.099 17.151,21.65 11.546,21.65C5.94,21.65 1.389,17.099 1.389,11.493C1.389,5.888 5.94,1.337 11.546,1.337ZM10.288,13.666C10.735,14.248 11.352,14.54 12.138,14.54C12.878,14.54 13.491,14.29 13.975,13.792C14.46,13.293 14.702,12.697 14.702,12.003C14.702,11.141 14.3,10.526 13.495,10.158C13.924,9.994 14.274,9.743 14.544,9.407C14.814,9.071 14.949,8.687 14.949,8.254C14.949,7.757 14.757,7.325 14.375,6.959C13.992,6.593 13.445,6.409 12.734,6.409C11.929,6.409 11.274,6.652 10.768,7.136C10.262,7.62 9.882,8.454 9.628,9.638L8.143,16.578L9.666,16.578L10.288,13.666ZM11.8,9.445L11.532,10.619C11.611,10.616 11.672,10.614 11.715,10.614C12.741,10.614 13.254,11.043 13.254,11.901C13.254,12.398 13.108,12.773 12.817,13.025C12.525,13.277 12.221,13.403 11.902,13.403C11.566,13.403 11.283,13.287 11.052,13.057C10.822,12.826 10.706,12.491 10.706,12.051C10.706,11.819 10.744,11.524 10.819,11.166L11.162,9.595C11.266,9.112 11.373,8.744 11.484,8.49C11.595,8.236 11.76,8.03 11.98,7.871C12.2,7.712 12.449,7.632 12.728,7.632C12.953,7.632 13.137,7.698 13.278,7.831C13.419,7.963 13.49,8.124 13.49,8.313C13.49,8.592 13.372,8.851 13.136,9.091C12.9,9.33 12.53,9.45 12.026,9.45C11.983,9.45 11.908,9.448 11.8,9.445Z" fill="#5C6F8A"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

+19 -31
View File
@@ -1,33 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 318.32 108.98">
<defs>
<style>
.cls-1 {
fill: #fe0032;
}
.cls-2 {
fill: #fff;
}
</style>
</defs>
<g>
<rect class="cls-1" x="7.1" y="7.8" width="5.84" height="5.48"/>
<rect class="cls-1" x="7.1" y="18.78" width="24.02" height="5.48"/>
<rect class="cls-1" x="7.1" y="29.77" width="42.85" height="5.48"/>
<rect class="cls-1" x="7.1" y="40.76" width="61.68" height="5.48"/>
<rect class="cls-1" x="7.1" y="51.75" width="80.88" height="5.48"/>
<rect class="cls-1" x="7.1" y="62.74" width="61.68" height="5.48"/>
<rect class="cls-1" x="7.1" y="73.73" width="42.85" height="5.48"/>
<rect class="cls-1" x="7.1" y="84.72" width="24.02" height="5.48"/>
<rect class="cls-1" x="7.1" y="95.71" width="5.84" height="5.48"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 318.32 108.98">
<g class="bv-logo__accent">
<rect x="7.1" y="7.8" width="5.84" height="5.48" />
<rect x="7.1" y="18.78" width="24.02" height="5.48" />
<rect x="7.1" y="29.77" width="42.85" height="5.48" />
<rect x="7.1" y="40.76" width="61.68" height="5.48" />
<rect x="7.1" y="51.75" width="80.88" height="5.48" />
<rect x="7.1" y="62.74" width="61.68" height="5.48" />
<rect x="7.1" y="73.73" width="42.85" height="5.48" />
<rect x="7.1" y="84.72" width="24.02" height="5.48" />
<rect x="7.1" y="95.71" width="5.84" height="5.48" />
</g>
<g>
<path class="cls-2" d="M121.28,25.08v44.54c0,5.67,2.79,8.54,8.22,8.54h11.57c5.51,0,8.3-2.87,8.3-8.54v-13.09c0-5.67-2.79-8.46-8.3-8.46h-16.6v-5.83h17.72c8.7,0,13.01,4.47,13.01,13.33v15.01c0,8.86-4.31,13.33-13.01,13.33h-13.73c-8.7,0-13.01-4.47-13.01-13.33V25.08h5.83Z"/>
<path class="cls-2" d="M168.53,25.08v6.26h-5.83v-6.26h5.83ZM168.53,42.64v41.27h-5.83v-41.27h5.83Z"/>
<path class="cls-2" d="M191.36,25.08v17.16h9.02v5.19h-9.02v36.48h-5.83v-36.48h-9.82v-5.19h9.82v-17.16h5.83Z"/>
<path class="cls-2" d="M212.88,42.24l16.84,35.68,16.6-35.68h6.07l-19.56,41.67h-6.31l-20.04-41.67h6.39Z"/>
<path class="cls-2" d="M264.05,25.08v6.26h-5.83v-6.26h5.83ZM264.05,42.24v41.67h-5.83v-41.67h5.83Z"/>
<path class="cls-2" d="M311.21,25.08v45.5c0,8.86-4.31,13.33-13.01,13.33h-13.73c-8.7,0-13.01-4.47-13.01-13.33v-15.01c0-8.86,4.31-13.33,13.01-13.33h17.72v5.83h-16.6c-5.51,0-8.3,2.79-8.3,8.46v13.09c0,5.67,2.79,8.54,8.3,8.54h11.49c5.51,0,8.3-2.87,8.3-8.54V25.08h5.83Z"/>
<g class="bv-logo__wordmark">
<path d="M121.28 25.08v44.54c0 5.67 2.79 8.54 8.22 8.54h11.57c5.51 0 8.3-2.87 8.3-8.54v-13.09c0-5.67-2.79-8.46-8.3-8.46h-16.6v-5.83h17.72c8.7 0 13.01 4.47 13.01 13.33v15.01c0 8.86-4.31 13.33-13.01 13.33h-13.73c-8.7 0-13.01-4.47-13.01-13.33V25.08h5.83Z" />
<path d="M168.53 25.08v6.26h-5.83v-6.26h5.83Zm0 17.56v41.27h-5.83v-41.27h5.83Z" />
<path d="M191.36 25.08v17.16h9.02v5.19h-9.02v36.48h-5.83v-36.48h-9.82v-5.19h9.82V25.08h5.83Z" />
<path d="M212.88 42.24l16.84 35.68 16.6-35.68h6.07l-19.56 41.67h-6.31l-20.04-41.67h6.39Z" />
<path d="M264.05 25.08v6.26h-5.83v-6.26h5.83Zm0 17.16v41.67h-5.83v-41.67h5.83Z" />
<path d="M311.21 25.08v45.5c0 8.86-4.31 13.33-13.01 13.33h-13.73c-8.7 0-13.01-4.47-13.01-13.33v-15.01c0-8.86 4.31-13.33 13.01-13.33h17.72v5.83h-16.6c-5.51 0-8.3 2.79-8.3 8.46v13.09c0 5.67 2.79 8.54 8.3 8.54h11.49c5.51 0 8.3-2.87 8.3-8.54V25.08h5.83Z" />
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

+19 -31
View File
@@ -1,33 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 318.32 108.98">
<defs>
<style>
.cls-1 {
fill: #fe0032;
}
.cls-2 {
fill: #383838;
}
</style>
</defs>
<g>
<rect class="cls-1" x="7.1" y="7.8" width="5.84" height="5.48"/>
<rect class="cls-1" x="7.1" y="18.78" width="24.02" height="5.48"/>
<rect class="cls-1" x="7.1" y="29.77" width="42.85" height="5.48"/>
<rect class="cls-1" x="7.1" y="40.76" width="61.68" height="5.48"/>
<rect class="cls-1" x="7.1" y="51.75" width="80.88" height="5.48"/>
<rect class="cls-1" x="7.1" y="62.74" width="61.68" height="5.48"/>
<rect class="cls-1" x="7.1" y="73.73" width="42.85" height="5.48"/>
<rect class="cls-1" x="7.1" y="84.72" width="24.02" height="5.48"/>
<rect class="cls-1" x="7.1" y="95.71" width="5.84" height="5.48"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 318.32 108.98">
<g class="bv-logo__accent">
<rect x="7.1" y="7.8" width="5.84" height="5.48" />
<rect x="7.1" y="18.78" width="24.02" height="5.48" />
<rect x="7.1" y="29.77" width="42.85" height="5.48" />
<rect x="7.1" y="40.76" width="61.68" height="5.48" />
<rect x="7.1" y="51.75" width="80.88" height="5.48" />
<rect x="7.1" y="62.74" width="61.68" height="5.48" />
<rect x="7.1" y="73.73" width="42.85" height="5.48" />
<rect x="7.1" y="84.72" width="24.02" height="5.48" />
<rect x="7.1" y="95.71" width="5.84" height="5.48" />
</g>
<g>
<path class="cls-2" d="M121.28,25.08v44.54c0,5.67,2.79,8.54,8.22,8.54h11.57c5.51,0,8.3-2.87,8.3-8.54v-13.09c0-5.67-2.79-8.46-8.3-8.46h-16.6v-5.83h17.72c8.7,0,13.01,4.47,13.01,13.33v15.01c0,8.86-4.31,13.33-13.01,13.33h-13.73c-8.7,0-13.01-4.47-13.01-13.33V25.08h5.83Z"/>
<path class="cls-2" d="M168.53,25.08v6.26h-5.83v-6.26h5.83ZM168.53,42.64v41.27h-5.83v-41.27h5.83Z"/>
<path class="cls-2" d="M191.36,25.08v17.16h9.02v5.19h-9.02v36.48h-5.83v-36.48h-9.82v-5.19h9.82v-17.16h5.83Z"/>
<path class="cls-2" d="M212.88,42.24l16.84,35.68,16.6-35.68h6.07l-19.56,41.67h-6.31l-20.04-41.67h6.39Z"/>
<path class="cls-2" d="M264.05,25.08v6.26h-5.83v-6.26h5.83ZM264.05,42.24v41.67h-5.83v-41.67h5.83Z"/>
<path class="cls-2" d="M311.21,25.08v45.5c0,8.86-4.31,13.33-13.01,13.33h-13.73c-8.7,0-13.01-4.47-13.01-13.33v-15.01c0-8.86,4.31-13.33,13.01-13.33h17.72v5.83h-16.6c-5.51,0-8.3,2.79-8.3,8.46v13.09c0,5.67,2.79,8.54,8.3,8.54h11.49c5.51,0,8.3-2.87,8.3-8.54V25.08h5.83Z"/>
<g class="bv-logo__wordmark">
<path d="M121.28 25.08v44.54c0 5.67 2.79 8.54 8.22 8.54h11.57c5.51 0 8.3-2.87 8.3-8.54v-13.09c0-5.67-2.79-8.46-8.3-8.46h-16.6v-5.83h17.72c8.7 0 13.01 4.47 13.01 13.33v15.01c0 8.86-4.31 13.33-13.01 13.33h-13.73c-8.7 0-13.01-4.47-13.01-13.33V25.08h5.83Z" />
<path d="M168.53 25.08v6.26h-5.83v-6.26h5.83Zm0 17.56v41.27h-5.83v-41.27h5.83Z" />
<path d="M191.36 25.08v17.16h9.02v5.19h-9.02v36.48h-5.83v-36.48h-9.82v-5.19h9.82V25.08h5.83Z" />
<path d="M212.88 42.24l16.84 35.68 16.6-35.68h6.07l-19.56 41.67h-6.31l-20.04-41.67h6.39Z" />
<path d="M264.05 25.08v6.26h-5.83v-6.26h5.83Zm0 17.16v41.67h-5.83v-41.67h5.83Z" />
<path d="M311.21 25.08v45.5c0 8.86-4.31 13.33-13.01 13.33h-13.73c-8.7 0-13.01-4.47-13.01-13.33v-15.01c0-8.86 4.31-13.33 13.01-13.33h17.72v5.83h-16.6c-5.51 0-8.3 2.79-8.3 8.46v13.09c0 5.67 2.79 8.54 8.3 8.54h11.49c5.51 0 8.3-2.87 8.3-8.54V25.08h5.83Z" />
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

+2 -2
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M13.744,8C13.744,8 15.266,0 10.409,0L2,0L2,24L22,24L22,11C22,7.581 16.753,7.255 13.744,8ZM18,19L6,19L6,18L18,18L18,19ZM18,16L6,16L6,15L18,15L18,16ZM18,13L6,13L6,12L18,12L18,13ZM14.568,0.075C16.77,1.249 20.506,4.958 22,6.956C20.714,6.056 17.956,5.299 15.909,5.777C16.131,4.309 15.724,1.243 14.568,0.075Z" style="fill:rgb(92,111,138);fill-rule:nonzero;"/>
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2">
<path d="M13.744,8C13.744,8 15.266,0 10.409,0L2,0L2,24L22,24L22,11C22,7.581 16.753,7.255 13.744,8ZM18,19L6,19L6,18L18,18L18,19ZM18,16L6,16L6,15L18,15L18,16ZM18,13L6,13L6,12L18,12L18,13ZM14.568,0.075C16.77,1.249 20.506,4.958 22,6.956C20.714,6.056 17.956,5.299 15.909,5.777C16.131,4.309 15.724,1.243 14.568,0.075Z" fill="#5C6F8A" fill-rule="nonzero"/>
</svg>

Before

Width:  |  Height:  |  Size: 817 B

After

Width:  |  Height:  |  Size: 803 B

+2 -2
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M12,0C5.373,0 0,5.373 0,12C0,18.627 5.373,24 12,24C18.627,24 24,18.627 24,12C24,5.373 18.627,0 12,0ZM12,22C8.877,22 6.086,20.559 4.251,18.31C4.51,17.722 5.034,17.315 6.118,17.064C8.362,16.546 10.577,16.083 9.511,14.119C6.356,8.299 8.612,5 12,5C15.322,5 17.634,8.177 14.489,14.119C13.454,16.071 15.589,16.535 17.882,17.064C18.964,17.314 19.492,17.719 19.753,18.305C17.917,20.558 15.125,22 12,22Z" style="fill:rgb(92,111,138);fill-rule:nonzero;"/>
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2">
<path d="M12,0C5.373,0 0,5.373 0,12C0,18.627 5.373,24 12,24C18.627,24 24,18.627 24,12C24,5.373 18.627,0 12,0ZM12,22C8.877,22 6.086,20.559 4.251,18.31C4.51,17.722 5.034,17.315 6.118,17.064C8.362,16.546 10.577,16.083 9.511,14.119C6.356,8.299 8.612,5 12,5C15.322,5 17.634,8.177 14.489,14.119C13.454,16.071 15.589,16.535 17.882,17.064C18.964,17.314 19.492,17.719 19.753,18.305C17.917,20.558 15.125,22 12,22Z" fill="#5C6F8A" fill-rule="nonzero"/>
</svg>

Before

Width:  |  Height:  |  Size: 909 B

After

Width:  |  Height:  |  Size: 895 B

+2 -2
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M12,0C18.623,0 24,5.377 24,12C24,18.623 18.623,24 12,24C5.377,24 0,18.623 0,12C0,5.377 5.377,0 12,0M14.557,16L9.442,16C9.988,18.46 10.883,20.114 12,21.744C13.194,20.003 14.041,18.334 14.557,16M7.4,16L2.833,16C4.069,18.825 6.537,20.972 9.588,21.716C8.54,19.983 7.805,18.058 7.4,16M21.167,16L16.6,16C16.209,17.988 15.505,19.887 14.425,21.694C17.437,20.931 19.942,18.799 21.167,16M7.162,10L2.2,10C1.933,11.313 1.933,12.685 2.2,14L7.115,14C6.996,12.671 7.014,11.328 7.162,10M14.823,10L9.176,10C9.011,11.326 8.991,12.672 9.123,14L14.876,14C15.009,12.672 14.987,11.327 14.823,10M21.8,10L16.837,10C16.985,11.328 17.003,12.671 16.885,14L21.8,14C22.06,12.715 22.073,11.352 21.8,10M9.644,2.271C6.567,3.003 4.077,5.157 2.833,8L7.486,8C7.921,5.958 8.664,4.015 9.644,2.271M11.999,2.223C10.91,3.993 10.089,5.676 9.536,8L14.463,8C13.929,5.754 13.126,4.052 11.999,2.223M14.367,2.292C15.38,4.104 16.1,6.052 16.513,8L21.167,8C19.935,5.184 17.405,3.042 14.367,2.292" style="fill:rgb(92,111,138);"/>
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2">
<path d="M12,0C18.623,0 24,5.377 24,12C24,18.623 18.623,24 12,24C5.377,24 0,18.623 0,12C0,5.377 5.377,0 12,0M14.557,16L9.442,16C9.988,18.46 10.883,20.114 12,21.744C13.194,20.003 14.041,18.334 14.557,16M7.4,16L2.833,16C4.069,18.825 6.537,20.972 9.588,21.716C8.54,19.983 7.805,18.058 7.4,16M21.167,16L16.6,16C16.209,17.988 15.505,19.887 14.425,21.694C17.437,20.931 19.942,18.799 21.167,16M7.162,10L2.2,10C1.933,11.313 1.933,12.685 2.2,14L7.115,14C6.996,12.671 7.014,11.328 7.162,10M14.823,10L9.176,10C9.011,11.326 8.991,12.672 9.123,14L14.876,14C15.009,12.672 14.987,11.327 14.823,10M21.8,10L16.837,10C16.985,11.328 17.003,12.671 16.885,14L21.8,14C22.06,12.715 22.073,11.352 21.8,10M9.644,2.271C6.567,3.003 4.077,5.157 2.833,8L7.486,8C7.921,5.958 8.664,4.015 9.644,2.271M11.999,2.223C10.91,3.993 10.089,5.676 9.536,8L14.463,8C13.929,5.754 13.126,4.052 11.999,2.223M14.367,2.292C15.38,4.104 16.1,6.052 16.513,8L21.167,8C19.935,5.184 17.405,3.042 14.367,2.292" fill="#5C6F8A"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

+2 -2
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M12,0C18.623,0 24,5.377 24,12C24,18.623 18.623,24 12,24C5.377,24 0,18.623 0,12C0,5.377 5.377,0 12,0ZM12,2C17.519,2 22,6.481 22,12C22,17.519 17.519,22 12,22C6.481,22 2,17.519 2,12C2,6.481 6.481,2 12,2ZM13.476,14.955C14.464,14.55 15.233,13.744 15.592,12.739L18,6L11.328,8.387C10.322,8.747 9.517,9.518 9.112,10.506L6.047,18L13.476,14.955ZM13.354,10.669C13.905,11.22 13.905,12.115 13.354,12.665C12.803,13.216 11.909,13.216 11.358,12.665C10.807,12.115 10.807,11.22 11.358,10.669C11.909,10.118 12.803,10.118 13.354,10.669Z" style="fill:rgb(92,111,138);"/>
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2">
<path d="M12,0C18.623,0 24,5.377 24,12C24,18.623 18.623,24 12,24C5.377,24 0,18.623 0,12C0,5.377 5.377,0 12,0ZM12,2C17.519,2 22,6.481 22,12C22,17.519 17.519,22 12,22C6.481,22 2,17.519 2,12C2,6.481 6.481,2 12,2ZM13.476,14.955C14.464,14.55 15.233,13.744 15.592,12.739L18,6L11.328,8.387C10.322,8.747 9.517,9.518 9.112,10.506L6.047,18L13.476,14.955ZM13.354,10.669C13.905,11.22 13.905,12.115 13.354,12.665C12.803,13.216 11.909,13.216 11.358,12.665C10.807,12.115 10.807,11.22 11.358,10.669C11.909,10.118 12.803,10.118 13.354,10.669Z" fill="#5C6F8A"/>
</svg>

Before

Width:  |  Height:  |  Size: 1013 B

After

Width:  |  Height:  |  Size: 997 B

+2 -2
View File
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2">
<g id="iconmonstr-weather-64.svg" transform="matrix(1,0,0,1,12,12)">
<g id="iconmonstr-filter-down-lined.svg" transform="matrix(1,0,0,1,-12,-12)">
<path d="M15.344,17.778C15.344,17.364 15.008,17.028 14.594,17.028L9.434,17.028C9.02,17.028 8.684,17.364 8.684,17.778C8.684,18.192 9.02,18.528 9.434,18.528L14.594,18.528C15.008,18.528 15.344,18.192 15.344,17.778ZM17.55,13.778C17.55,13.364 17.214,13.028 16.8,13.028L7.204,13.028C6.79,13.028 6.454,13.364 6.454,13.778C6.454,14.192 6.79,14.528 7.204,14.528L16.8,14.528C17.214,14.528 17.55,14.192 17.55,13.778ZM20,9.778C20,9.364 19.664,9.028 19.25,9.028L4.75,9.028C4.336,9.028 4,9.364 4,9.778C4,10.192 4.336,10.528 4.75,10.528L19.25,10.528C19.664,10.528 20,10.192 20,9.778ZM22,5.778C22,5.364 21.664,5.028 21.25,5.028L2.75,5.028C2.336,5.028 2,5.364 2,5.778C2,6.192 2.336,6.528 2.75,6.528L21.25,6.528C21.664,6.528 22,6.192 22,5.778Z" style="fill:rgb(92,111,138);fill-rule:nonzero;"/>
<path d="M15.344,17.778C15.344,17.364 15.008,17.028 14.594,17.028L9.434,17.028C9.02,17.028 8.684,17.364 8.684,17.778C8.684,18.192 9.02,18.528 9.434,18.528L14.594,18.528C15.008,18.528 15.344,18.192 15.344,17.778ZM17.55,13.778C17.55,13.364 17.214,13.028 16.8,13.028L7.204,13.028C6.79,13.028 6.454,13.364 6.454,13.778C6.454,14.192 6.79,14.528 7.204,14.528L16.8,14.528C17.214,14.528 17.55,14.192 17.55,13.778ZM20,9.778C20,9.364 19.664,9.028 19.25,9.028L4.75,9.028C4.336,9.028 4,9.364 4,9.778C4,10.192 4.336,10.528 4.75,10.528L19.25,10.528C19.664,10.528 20,10.192 20,9.778ZM22,5.778C22,5.364 21.664,5.028 21.25,5.028L2.75,5.028C2.336,5.028 2,5.364 2,5.778C2,6.192 2.336,6.528 2.75,6.528L21.25,6.528C21.664,6.528 22,6.192 22,5.778Z" fill="#5C6F8A" fill-rule="nonzero"/>
</g>
<g id="iconmonstr-filter-down-lined.svg1" serif:id="iconmonstr-filter-down-lined.svg">
</g>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M5 5a2 2 0 1 1 4 0h9a1 1 0 1 1 0 2H9a2 2 0 1 1-4 0H4a1 1 0 1 1 0-2h1Zm10 6a2 2 0 1 1 4 0h1a1 1 0 1 1 0 2h-1a2 2 0 1 1-4 0H4a1 1 0 1 1 0-2h11Zm-6 7a2 2 0 1 1 4 0h6a1 1 0 1 1 0 2h-6a2 2 0 1 1-4 0H4a1 1 0 1 1 0-2h5Z" />
</svg>

After

Width:  |  Height:  |  Size: 316 B

+2 -2
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M6.676,9.18C5.25,9.171 3.459,9.944 2.093,11.31C1.572,11.831 1.114,12.439 0.76,13.122C1.992,12.189 3.307,11.897 4.846,12.761C5.299,11.562 5.902,10.343 6.676,9.18ZM14.83,17.323C13.566,18.149 12.324,18.745 11.249,19.165C12.112,20.705 11.82,22.018 10.888,23.25C11.572,22.897 12.179,22.438 12.7,21.916C14.07,20.547 14.844,18.751 14.83,17.323ZM19.957,4.035C19.613,4.011 19.276,4 18.946,4C11.777,4 7.697,9.465 6.213,13.86L10.152,17.8C14.677,16.18 20,12.251 20,5.158C20,4.792 19.986,4.418 19.957,4.035ZM11.717,12.293C11.391,11.968 11.391,11.44 11.717,11.115C12.042,10.789 12.57,10.789 12.895,11.115C13.221,11.441 13.221,11.968 12.895,12.293C12.569,12.619 12.042,12.619 11.717,12.293ZM14.073,9.937C13.422,9.287 13.422,8.231 14.073,7.58C14.724,6.929 15.779,6.929 16.429,7.58C17.08,8.231 17.08,9.286 16.429,9.937C15.779,10.587 14.725,10.587 14.073,9.937ZM1.641,20.315L0.886,19.56L5.226,15.237L5.981,15.992L1.641,20.315ZM5.79,21.862L5.035,21.107L8.064,18.053L8.819,18.808L5.79,21.862ZM0.755,24L-0,23.245L5.373,17.881L6.129,18.636L0.755,24ZM21.838,9.709C21.65,10.327 21.165,10.811 20.547,11C21.165,11.188 21.65,11.672 21.838,12.291C22.027,11.672 22.511,11.188 23.129,11C22.511,10.812 22.027,10.328 21.838,9.709ZM7.183,3.205C6.936,4.015 6.302,4.648 5.493,4.895C6.303,5.142 6.936,5.776 7.183,6.585C7.431,5.776 8.064,5.142 8.873,4.895C8.063,4.648 7.431,4.015 7.183,3.205ZM5.356,0C5.157,0.649 4.65,1.157 4,1.355C4.65,1.554 5.157,2.062 5.356,2.71C5.554,2.061 6.062,1.553 6.71,1.355C6.062,1.157 5.555,0.649 5.356,0ZM10.743,0C10.427,1.035 9.616,1.846 8.58,2.163C9.616,2.479 10.427,3.289 10.743,4.326C11.059,3.29 11.87,2.48 12.905,2.163C11.87,1.846 11.06,1.035 10.743,0ZM21.838,13.64C21.522,14.676 20.711,15.486 19.675,15.803C20.711,16.119 21.522,16.965 21.838,18C22.154,16.964 22.965,16.119 24,15.803C22.965,15.486 22.154,14.676 21.838,13.64Z" style="fill:rgb(92,111,138);fill-rule:nonzero;"/>
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2">
<path d="M6.676,9.18C5.25,9.171 3.459,9.944 2.093,11.31C1.572,11.831 1.114,12.439 0.76,13.122C1.992,12.189 3.307,11.897 4.846,12.761C5.299,11.562 5.902,10.343 6.676,9.18ZM14.83,17.323C13.566,18.149 12.324,18.745 11.249,19.165C12.112,20.705 11.82,22.018 10.888,23.25C11.572,22.897 12.179,22.438 12.7,21.916C14.07,20.547 14.844,18.751 14.83,17.323ZM19.957,4.035C19.613,4.011 19.276,4 18.946,4C11.777,4 7.697,9.465 6.213,13.86L10.152,17.8C14.677,16.18 20,12.251 20,5.158C20,4.792 19.986,4.418 19.957,4.035ZM11.717,12.293C11.391,11.968 11.391,11.44 11.717,11.115C12.042,10.789 12.57,10.789 12.895,11.115C13.221,11.441 13.221,11.968 12.895,12.293C12.569,12.619 12.042,12.619 11.717,12.293ZM14.073,9.937C13.422,9.287 13.422,8.231 14.073,7.58C14.724,6.929 15.779,6.929 16.429,7.58C17.08,8.231 17.08,9.286 16.429,9.937C15.779,10.587 14.725,10.587 14.073,9.937ZM1.641,20.315L0.886,19.56L5.226,15.237L5.981,15.992L1.641,20.315ZM5.79,21.862L5.035,21.107L8.064,18.053L8.819,18.808L5.79,21.862ZM0.755,24L-0,23.245L5.373,17.881L6.129,18.636L0.755,24ZM21.838,9.709C21.65,10.327 21.165,10.811 20.547,11C21.165,11.188 21.65,11.672 21.838,12.291C22.027,11.672 22.511,11.188 23.129,11C22.511,10.812 22.027,10.328 21.838,9.709ZM7.183,3.205C6.936,4.015 6.302,4.648 5.493,4.895C6.303,5.142 6.936,5.776 7.183,6.585C7.431,5.776 8.064,5.142 8.873,4.895C8.063,4.648 7.431,4.015 7.183,3.205ZM5.356,0C5.157,0.649 4.65,1.157 4,1.355C4.65,1.554 5.157,2.062 5.356,2.71C5.554,2.061 6.062,1.553 6.71,1.355C6.062,1.157 5.555,0.649 5.356,0ZM10.743,0C10.427,1.035 9.616,1.846 8.58,2.163C9.616,2.479 10.427,3.289 10.743,4.326C11.059,3.29 11.87,2.48 12.905,2.163C11.87,1.846 11.06,1.035 10.743,0ZM21.838,13.64C21.522,14.676 20.711,15.486 19.675,15.803C20.711,16.119 21.522,16.965 21.838,18C22.154,16.964 22.965,16.119 24,15.803C22.965,15.486 22.154,14.676 21.838,13.64Z" fill="#5C6F8A" fill-rule="nonzero"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

+2 -2
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M12,0C5.374,0 0,5.373 0,12C0,17.302 3.438,21.8 8.207,23.387C8.806,23.498 9,23.126 9,22.81L9,20.576C5.662,21.302 4.967,19.16 4.967,19.16C4.421,17.773 3.634,17.404 3.634,17.404C2.545,16.659 3.717,16.675 3.717,16.675C4.922,16.759 5.556,17.912 5.556,17.912C6.626,19.746 8.363,19.216 9.048,18.909C9.155,18.134 9.466,17.604 9.81,17.305C7.145,17 4.343,15.971 4.343,11.374C4.343,10.063 4.812,8.993 5.579,8.153C5.455,7.85 5.044,6.629 5.696,4.977C5.696,4.977 6.704,4.655 8.997,6.207C9.954,5.941 10.98,5.808 12,5.803C13.02,5.808 14.047,5.941 15.006,6.207C17.297,4.655 18.303,4.977 18.303,4.977C18.956,6.63 18.545,7.851 18.421,8.153C19.191,8.993 19.656,10.064 19.656,11.374C19.656,15.983 16.849,16.998 14.177,17.295C14.607,17.667 15,18.397 15,19.517L15,22.81C15,23.129 15.192,23.504 15.801,23.386C20.566,21.797 24,17.3 24,12C24,5.373 18.627,0 12,0Z" style="fill:rgb(92,111,138);fill-rule:nonzero;"/>
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2">
<path d="M12,0C5.374,0 0,5.373 0,12C0,17.302 3.438,21.8 8.207,23.387C8.806,23.498 9,23.126 9,22.81L9,20.576C5.662,21.302 4.967,19.16 4.967,19.16C4.421,17.773 3.634,17.404 3.634,17.404C2.545,16.659 3.717,16.675 3.717,16.675C4.922,16.759 5.556,17.912 5.556,17.912C6.626,19.746 8.363,19.216 9.048,18.909C9.155,18.134 9.466,17.604 9.81,17.305C7.145,17 4.343,15.971 4.343,11.374C4.343,10.063 4.812,8.993 5.579,8.153C5.455,7.85 5.044,6.629 5.696,4.977C5.696,4.977 6.704,4.655 8.997,6.207C9.954,5.941 10.98,5.808 12,5.803C13.02,5.808 14.047,5.941 15.006,6.207C17.297,4.655 18.303,4.977 18.303,4.977C18.956,6.63 18.545,7.851 18.421,8.153C19.191,8.993 19.656,10.064 19.656,11.374C19.656,15.983 16.849,16.998 14.177,17.295C14.607,17.667 15,18.397 15,19.517L15,22.81C15,23.129 15.192,23.504 15.801,23.386C20.566,21.797 24,17.3 24,12C24,5.373 18.627,0 12,0Z" fill="#5C6F8A" fill-rule="nonzero"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

+2 -2
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M23.334,11.96C22.621,11.234 22.462,10.131 22.941,9.233C23.283,8.593 23.307,7.832 23.005,7.171C22.704,6.511 22.112,6.029 21.404,5.869C20.413,5.644 19.682,4.802 19.601,3.788C19.542,3.065 19.15,2.41 18.539,2.018C17.93,1.625 17.172,1.54 16.489,1.789C15.533,2.136 14.463,1.821 13.847,1.013C13.407,0.437 12.723,0.098 11.997,0.098C11.272,0.098 10.588,0.437 10.148,1.013C9.535,1.822 8.465,2.137 7.509,1.79C6.827,1.542 6.069,1.627 5.459,2.019C4.849,2.411 4.456,3.066 4.398,3.789C4.316,4.803 3.586,5.646 2.595,5.87C1.887,6.03 1.295,6.512 0.994,7.172C0.693,7.832 0.717,8.594 1.059,9.233C1.538,10.13 1.379,11.234 0.667,11.96C0.158,12.477 -0.08,13.202 0.023,13.92C0.126,14.638 0.559,15.267 1.193,15.62C2.081,16.115 2.545,17.13 2.337,18.125C2.19,18.835 2.381,19.573 2.856,20.121C3.332,20.67 4.036,20.965 4.758,20.919C5.774,20.856 6.711,21.459 7.075,22.408C7.334,23.086 7.895,23.603 8.592,23.807C9.287,24.011 10.039,23.879 10.623,23.45C11.442,22.847 12.559,22.847 13.377,23.45C13.961,23.88 14.713,24.012 15.408,23.807C16.105,23.603 16.666,23.085 16.926,22.408C17.289,21.459 18.227,20.855 19.242,20.919C19.966,20.965 20.669,20.67 21.144,20.121C21.619,19.573 21.811,18.835 21.663,18.125C21.456,17.13 21.919,16.115 22.808,15.62C23.441,15.266 23.873,14.638 23.977,13.92C24.081,13.202 23.842,12.477 23.334,11.96ZM10.75,17.39L6.25,13.026L8.107,11.169L10.75,13.675L16.393,7.891L18.25,9.748L10.75,17.39Z" style="fill:rgb(92,111,138);fill-rule:nonzero;"/>
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2">
<path d="M23.334,11.96C22.621,11.234 22.462,10.131 22.941,9.233C23.283,8.593 23.307,7.832 23.005,7.171C22.704,6.511 22.112,6.029 21.404,5.869C20.413,5.644 19.682,4.802 19.601,3.788C19.542,3.065 19.15,2.41 18.539,2.018C17.93,1.625 17.172,1.54 16.489,1.789C15.533,2.136 14.463,1.821 13.847,1.013C13.407,0.437 12.723,0.098 11.997,0.098C11.272,0.098 10.588,0.437 10.148,1.013C9.535,1.822 8.465,2.137 7.509,1.79C6.827,1.542 6.069,1.627 5.459,2.019C4.849,2.411 4.456,3.066 4.398,3.789C4.316,4.803 3.586,5.646 2.595,5.87C1.887,6.03 1.295,6.512 0.994,7.172C0.693,7.832 0.717,8.594 1.059,9.233C1.538,10.13 1.379,11.234 0.667,11.96C0.158,12.477 -0.08,13.202 0.023,13.92C0.126,14.638 0.559,15.267 1.193,15.62C2.081,16.115 2.545,17.13 2.337,18.125C2.19,18.835 2.381,19.573 2.856,20.121C3.332,20.67 4.036,20.965 4.758,20.919C5.774,20.856 6.711,21.459 7.075,22.408C7.334,23.086 7.895,23.603 8.592,23.807C9.287,24.011 10.039,23.879 10.623,23.45C11.442,22.847 12.559,22.847 13.377,23.45C13.961,23.88 14.713,24.012 15.408,23.807C16.105,23.603 16.666,23.085 16.926,22.408C17.289,21.459 18.227,20.855 19.242,20.919C19.966,20.965 20.669,20.67 21.144,20.121C21.619,19.573 21.811,18.835 21.663,18.125C21.456,17.13 21.919,16.115 22.808,15.62C23.441,15.266 23.873,14.638 23.977,13.92C24.081,13.202 23.842,12.477 23.334,11.96ZM10.75,17.39L6.25,13.026L8.107,11.169L10.75,13.675L16.393,7.891L18.25,9.748L10.75,17.39Z" fill="#5C6F8A" fill-rule="nonzero"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

+2 -2
View File
@@ -9,13 +9,13 @@
xmlns:xlink="http://www.w3.org/1999/xlink"
xml:space="preserve"
xmlns:serif="http://www.serif.com/"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"
fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2"
>
<g transform="matrix(1,0,0,1,12,12)">
<g transform="matrix(1,0,0,1,-12,-12)">
<path
d="M12,2C6.486,2 2,6.486 2,12C2,17.514 6.486,22 12,22C17.514,22 22,17.514 22,12C22,6.486 17.514,2 12,2ZM12,4C16.411,4 20,7.589 20,12C20,16.411 16.411,20 12,20C7.589,20 4,16.411 4,12C4,7.589 7.589,4 12,4ZM11,7C10.448,7 10,7.448 10,8L10,13C10,13.266 10.105,13.52 10.293,13.707L13.293,16.707C13.683,17.098 14.317,17.098 14.707,16.707C15.098,16.317 15.098,15.683 14.707,15.293L12,12.586L12,8C12,7.448 11.552,7 11,7Z"
style="fill:rgb(92,111,138);fill-rule:nonzero;"
fill="#5C6F8A" fill-rule="nonzero"
/>
</g>
</g>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

+2 -2
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M12,6.453L21,14.828L21,24L15,24L15,18L9,18L9,24L3,24L3,14.828L12,6.453ZM24,12.148L12,1L0,12.133L1.361,13.598L12,3.73L22.639,13.613L24,12.148Z" style="fill:rgb(92,111,138);fill-rule:nonzero;"/>
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2">
<path d="M12,6.453L21,14.828L21,24L15,24L15,18L9,18L9,24L3,24L3,14.828L12,6.453ZM24,12.148L12,1L0,12.133L1.361,13.598L12,3.73L22.639,13.613L24,12.148Z" fill="#5C6F8A" fill-rule="nonzero"/>
</svg>

Before

Width:  |  Height:  |  Size: 656 B

After

Width:  |  Height:  |  Size: 642 B

+2 -2
View File
@@ -1,13 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2">
<g id="iconmonstr-weather-64.svg" transform="matrix(1,0,0,1,12,12)">
<g id="iconmonstr-whats-hot-1.svg" transform="matrix(1,0,0,1,-12,-12)">
<clipPath id="_clip1">
<rect x="0" y="0" width="24" height="24"/>
</clipPath>
<g clip-path="url(#_clip1)">
<path d="M8.625,0C9.235,7.189 3,9.664 3,15.996C3,20.297 6.069,23.968 12,24C17.931,24.032 21,19.586 21,15.044C21,10.903 18.938,6.998 15.048,4.57C15.972,7.177 14.742,9.558 13.547,10.378C13.617,7.041 12.422,2.089 8.625,0ZM13.336,13C17.091,16.989 14.785,22 11.769,22C9.934,22 8.99,20.735 9,19.423C9.019,16.99 11.737,16.988 13.336,13Z" style="fill:rgb(92,111,138);"/>
<path d="M8.625,0C9.235,7.189 3,9.664 3,15.996C3,20.297 6.069,23.968 12,24C17.931,24.032 21,19.586 21,15.044C21,10.903 18.938,6.998 15.048,4.57C15.972,7.177 14.742,9.558 13.547,10.378C13.617,7.041 12.422,2.089 8.625,0ZM13.336,13C17.091,16.989 14.785,22 11.769,22C9.934,22 8.99,20.735 9,19.423C9.019,16.99 11.737,16.988 13.336,13Z" fill="#5C6F8A"/>
</g>
</g>
<g id="iconmonstr-whats-hot-1.svg1" serif:id="iconmonstr-whats-hot-1.svg">

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

+2 -2
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M21,7.702L12.5,12.322L12.5,22C14.067,21.135 18.879,18.483 20.477,17.601C20.8,17.424 21,17.082 21,16.71L21,7.702ZM11.5,12.321L3,7.599L3,16.605C3,16.975 3.197,17.313 3.514,17.492C5.104,18.39 9.93,21.115 11.5,22L11.5,12.321ZM3.421,6.692L12,11.455L20.672,6.742C20.672,6.742 14.041,3.004 12.486,2.128C12.335,2.043 12.167,2 12,2C11.832,2 11.665,2.043 11.514,2.128C9.959,3.004 3.421,6.692 3.421,6.692Z" style="fill:rgb(92,111,138);fill-rule:nonzero;"/>
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2">
<path d="M21,7.702L12.5,12.322L12.5,22C14.067,21.135 18.879,18.483 20.477,17.601C20.8,17.424 21,17.082 21,16.71L21,7.702ZM11.5,12.321L3,7.599L3,16.605C3,16.975 3.197,17.313 3.514,17.492C5.104,18.39 9.93,21.115 11.5,22L11.5,12.321ZM3.421,6.692L12,11.455L20.672,6.742C20.672,6.742 14.041,3.004 12.486,2.128C12.335,2.043 12.167,2 12,2C11.832,2 11.665,2.043 11.514,2.128C9.959,3.004 3.421,6.692 3.421,6.692Z" fill="#5C6F8A" fill-rule="nonzero"/>
</svg>

Before

Width:  |  Height:  |  Size: 909 B

After

Width:  |  Height:  |  Size: 895 B

+2 -2
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M21.337,0.458L17.4,4.394C14.951,2.928 11.73,3.25 9.62,5.36L5.359,9.622C3.249,11.732 2.926,14.953 4.391,17.402L0.456,21.337C-0.153,21.946 -0.153,22.934 0.456,23.544C1.065,24.154 2.054,24.153 2.663,23.544L6.598,19.609C9.047,21.074 12.268,20.753 14.378,18.642L18.639,14.381C20.749,12.271 21.072,9.05 19.607,6.6L23.543,2.664C24.152,2.055 24.152,1.067 23.543,0.457C22.934,-0.153 21.946,-0.152 21.337,0.458ZM16.475,12.215L12.214,16.476C11.319,17.37 10.038,17.645 8.904,17.303L10.536,15.671C11.145,15.062 11.145,14.074 10.536,13.464C9.927,12.854 8.938,12.855 8.329,13.464L6.698,15.096C6.355,13.963 6.631,12.681 7.524,11.787L11.785,7.526C12.68,6.632 13.961,6.357 15.095,6.699L13.464,8.329C12.855,8.938 12.855,9.926 13.464,10.536C14.073,11.146 15.062,11.145 15.671,10.536L17.301,8.906C17.644,10.039 17.368,11.321 16.475,12.215Z" style="fill:rgb(92,111,138);fill-rule:nonzero;"/>
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2">
<path d="M21.337,0.458L17.4,4.394C14.951,2.928 11.73,3.25 9.62,5.36L5.359,9.622C3.249,11.732 2.926,14.953 4.391,17.402L0.456,21.337C-0.153,21.946 -0.153,22.934 0.456,23.544C1.065,24.154 2.054,24.153 2.663,23.544L6.598,19.609C9.047,21.074 12.268,20.753 14.378,18.642L18.639,14.381C20.749,12.271 21.072,9.05 19.607,6.6L23.543,2.664C24.152,2.055 24.152,1.067 23.543,0.457C22.934,-0.153 21.946,-0.152 21.337,0.458ZM16.475,12.215L12.214,16.476C11.319,17.37 10.038,17.645 8.904,17.303L10.536,15.671C11.145,15.062 11.145,14.074 10.536,13.464C9.927,12.854 8.938,12.855 8.329,13.464L6.698,15.096C6.355,13.963 6.631,12.681 7.524,11.787L11.785,7.526C12.68,6.632 13.961,6.357 15.095,6.699L13.464,8.329C12.855,8.938 12.855,9.926 13.464,10.536C14.073,11.146 15.062,11.145 15.671,10.536L17.301,8.906C17.644,10.039 17.368,11.321 16.475,12.215Z" fill="#5C6F8A" fill-rule="nonzero"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

+2 -2
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M22,16.75C22,16.336 21.664,16 21.25,16L2.75,16C2.336,16 2,16.336 2,16.75C2,17.164 2.336,17.5 2.75,17.5L21.25,17.5C21.664,17.5 22,17.164 22,16.75ZM22,11.75C22,11.336 21.664,11 21.25,11L2.75,11C2.336,11 2,11.336 2,11.75C2,12.164 2.336,12.5 2.75,12.5L21.25,12.5C21.664,12.5 22,12.164 22,11.75ZM22,6.75C22,6.336 21.664,6 21.25,6L2.75,6C2.336,6 2,6.336 2,6.75C2,7.164 2.336,7.5 2.75,7.5L21.25,7.5C21.664,7.5 22,7.164 22,6.75Z" style="fill:rgb(17,24,39);fill-rule:nonzero;"/>
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2">
<path d="M22,16.75C22,16.336 21.664,16 21.25,16L2.75,16C2.336,16 2,16.336 2,16.75C2,17.164 2.336,17.5 2.75,17.5L21.25,17.5C21.664,17.5 22,17.164 22,16.75ZM22,11.75C22,11.336 21.664,11 21.25,11L2.75,11C2.336,11 2,11.336 2,11.75C2,12.164 2.336,12.5 2.75,12.5L21.25,12.5C21.664,12.5 22,12.164 22,11.75ZM22,6.75C22,6.336 21.664,6 21.25,6L2.75,6C2.336,6 2,6.336 2,6.75C2,7.164 2.336,7.5 2.75,7.5L21.25,7.5C21.664,7.5 22,7.164 22,6.75Z" fill="#111827" fill-rule="nonzero"/>
</svg>

Before

Width:  |  Height:  |  Size: 933 B

After

Width:  |  Height:  |  Size: 921 B

+3 -3
View File
@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2">
<g id="Artboard1" transform="matrix(0.217336,0,0,0.428004,4.79827,-2.20936)">
<rect x="-22.078" y="5.162" width="110.428" height="56.074" style="fill:none;"/>
<rect x="-22.078" y="5.162" width="110.428" height="56.074" fill="none"/>
<g id="nostr-head.svg" transform="matrix(0.143477,0,0,0.0728563,33.1364,33.1991)">
<g transform="matrix(1,0,0,1,-310,-310)">
<path d="M620,270.227L620,597.655C620,609.968 610.081,619.961 597.859,619.961L332.161,619.961C319.938,619.961 310.02,609.968 310.02,597.655L310.02,536.678C311.23,461.931 319.079,390.332 335.558,357.759C345.438,338.168 361.722,327.506 380.427,321.802C415.768,311.102 477.779,318.419 504.099,317.16C504.099,317.16 583.605,320.346 583.605,274.987C583.605,238.48 548.07,241.352 548.07,241.352C508.902,242.374 479.068,239.699 459.738,232.028C427.365,219.203 426.272,195.678 426.155,187.81C424.554,96.934 291.549,86.037 174.359,108.579C46.235,133.127 175.765,318.143 175.765,565.121L175.765,598.088C175.531,610.204 165.807,620 153.702,620L22.142,620C9.919,620 0,610.008 0,597.694L0,31.393C0,19.08 9.919,9.088 22.142,9.088L145.813,9.088C158.036,9.088 167.955,19.08 167.955,31.393C167.955,49.687 188.378,59.876 203.139,49.215C247.617,17.113 304.709,0 368.595,0C511.714,0 619.922,84.031 619.922,270.227L620,270.227ZM382.419,203.782C382.419,177.424 361.214,156.062 335.051,156.062C308.887,156.062 287.683,177.424 287.683,203.782C287.683,230.14 308.887,251.501 335.051,251.501C361.214,251.501 382.419,230.14 382.419,203.782Z" style="fill:rgb(92,111,138);fill-rule:nonzero;"/>
<path d="M620,270.227L620,597.655C620,609.968 610.081,619.961 597.859,619.961L332.161,619.961C319.938,619.961 310.02,609.968 310.02,597.655L310.02,536.678C311.23,461.931 319.079,390.332 335.558,357.759C345.438,338.168 361.722,327.506 380.427,321.802C415.768,311.102 477.779,318.419 504.099,317.16C504.099,317.16 583.605,320.346 583.605,274.987C583.605,238.48 548.07,241.352 548.07,241.352C508.902,242.374 479.068,239.699 459.738,232.028C427.365,219.203 426.272,195.678 426.155,187.81C424.554,96.934 291.549,86.037 174.359,108.579C46.235,133.127 175.765,318.143 175.765,565.121L175.765,598.088C175.531,610.204 165.807,620 153.702,620L22.142,620C9.919,620 0,610.008 0,597.694L0,31.393C0,19.08 9.919,9.088 22.142,9.088L145.813,9.088C158.036,9.088 167.955,19.08 167.955,31.393C167.955,49.687 188.378,59.876 203.139,49.215C247.617,17.113 304.709,0 368.595,0C511.714,0 619.922,84.031 619.922,270.227L620,270.227ZM382.419,203.782C382.419,177.424 361.214,156.062 335.051,156.062C308.887,156.062 287.683,177.424 287.683,203.782C287.683,230.14 308.887,251.501 335.051,251.501C361.214,251.501 382.419,230.14 382.419,203.782Z" fill="#5C6F8A" fill-rule="nonzero"/>
</g>
</g>
</g>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

+2 -2
View File
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2">
<g id="iconmonstr-weather-64.svg" transform="matrix(1,0,0,1,12,12)">
<g transform="matrix(1,0,0,1,-12,-12)">
<path d="M11,10L0,10L0,8L11,8C11.552,8 12,7.552 12,7C12,6.448 11.552,6 11,6C10.597,6 10.253,6.242 10.095,6.587L8.346,5.631C8.845,4.666 9.84,4 11,4C14.971,4 14.969,10 11,10ZM18,17C18,15.344 16.656,14 15,14L0,14L0,16L15,16C15.552,16 16,16.448 16,17C16,17.552 15.552,18 15,18C14.597,18 14.253,17.758 14.095,17.413L12.346,18.369C12.845,19.334 13.84,20 15,20C16.656,20 18,18.656 18,17ZM19.014,9.345C19.096,8.592 19.726,8 20.5,8C21.327,8 22,8.673 22,9.5C22,10.327 21.327,11 20.5,11L0,11L0,13L20.5,13C22.432,13 24,11.432 24,9.5C24,7.568 22.432,6 20.5,6C18.876,6 17.523,7.116 17.128,8.617L19.014,9.345Z" style="fill:rgb(92,111,138);fill-rule:nonzero;"/>
<path d="M11,10L0,10L0,8L11,8C11.552,8 12,7.552 12,7C12,6.448 11.552,6 11,6C10.597,6 10.253,6.242 10.095,6.587L8.346,5.631C8.845,4.666 9.84,4 11,4C14.971,4 14.969,10 11,10ZM18,17C18,15.344 16.656,14 15,14L0,14L0,16L15,16C15.552,16 16,16.448 16,17C16,17.552 15.552,18 15,18C14.597,18 14.253,17.758 14.095,17.413L12.346,18.369C12.845,19.334 13.84,20 15,20C16.656,20 18,18.656 18,17ZM19.014,9.345C19.096,8.592 19.726,8 20.5,8C21.327,8 22,8.673 22,9.5C22,10.327 21.327,11 20.5,11L0,11L0,13L20.5,13C22.432,13 24,11.432 24,9.5C24,7.568 22.432,6 20.5,6C18.876,6 17.523,7.116 17.128,8.617L19.014,9.345Z" fill="#5C6F8A" fill-rule="nonzero"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

+2 -2
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M23.961,8.429C23.13,9.411 22.347,10.347 22,12.204L22,18.887L18,21.366L18,12.205C17.653,10.348 16.87,9.412 16.039,8.43C15.131,7.355 14,6.019 14,3.801L14.019,3.456L12,2L6.455,6L0,2L0,20L6.455,24L12,20L17.545,24L24,20L24,8.382L23.961,8.429ZM11,18.255L7,21.14L7,8.073L11,5.187L11,18.255ZM20,0C17.9,0 16,1.702 16,3.801C16,6.922 19.188,7.252 20,12C20.812,7.252 24,6.922 24,3.801C24,1.702 22.1,0 20,0ZM20,5.5C19.172,5.5 18.5,4.829 18.5,4C18.5,3.171 19.172,2.5 20,2.5C20.828,2.5 21.5,3.171 21.5,4C21.5,4.829 20.828,5.5 20,5.5Z" style="fill:rgb(92,111,138);fill-rule:nonzero;"/>
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2">
<path d="M23.961,8.429C23.13,9.411 22.347,10.347 22,12.204L22,18.887L18,21.366L18,12.205C17.653,10.348 16.87,9.412 16.039,8.43C15.131,7.355 14,6.019 14,3.801L14.019,3.456L12,2L6.455,6L0,2L0,20L6.455,24L12,20L17.545,24L24,20L24,8.382L23.961,8.429ZM11,18.255L7,21.14L7,8.073L11,5.187L11,18.255ZM20,0C17.9,0 16,1.702 16,3.801C16,6.922 19.188,7.252 20,12C20.812,7.252 24,6.922 24,3.801C24,1.702 22.1,0 20,0ZM20,5.5C19.172,5.5 18.5,4.829 18.5,4C18.5,3.171 19.172,2.5 20,2.5C20.828,2.5 21.5,3.171 21.5,4C21.5,4.829 20.828,5.5 20,5.5Z" fill="#5C6F8A" fill-rule="nonzero"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1019 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M23.822 20.88l-6.353-6.354c.93-1.465 1.467-3.2 1.467-5.059.001-5.219-4.247-9.467-9.468-9.467s-9.468 4.248-9.468 9.468c0 5.221 4.247 9.469 9.468 9.469 1.768 0 3.421-.487 4.839-1.333l6.396 6.396 3.119-3.12zm-20.294-11.412c0-3.273 2.665-5.938 5.939-5.938 3.275 0 5.94 2.664 5.94 5.938 0 3.275-2.665 5.939-5.94 5.939-3.274 0-5.939-2.664-5.939-5.939z" />
</svg>

After

Width:  |  Height:  |  Size: 472 B

+2 -2
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M21.698,10.658L24,12L11.998,19L0,12L2.301,10.658L11.998,16.316L21.698,10.658ZM11.998,21.315L2.301,15.657L0,17L11.998,24L24,17L21.698,15.658L11.998,21.315ZM24,7L11.998,-0L0,7L11.998,14L24,7Z" style="fill:white;fill-rule:nonzero;"/>
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2">
<path d="M21.698,10.658L24,12L11.998,19L0,12L2.301,10.658L11.998,16.316L21.698,10.658ZM11.998,21.315L2.301,15.657L0,17L11.998,24L24,17L21.698,15.658L11.998,21.315ZM24,7L11.998,-0L0,7L11.998,14L24,7Z" fill="white" fill-rule="nonzero"/>
</svg>

Before

Width:  |  Height:  |  Size: 694 B

After

Width:  |  Height:  |  Size: 688 B

+2 -2
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M21.698,10.658L24,12L11.998,19L0,12L2.301,10.658L11.998,16.316L21.698,10.658ZM11.998,21.315L2.301,15.657L0,17L11.998,24L24,17L21.698,15.658L11.998,21.315ZM24,7L11.998,-0L0,7L11.998,14L24,7Z" style="fill:rgb(92,111,138);fill-rule:nonzero;"/>
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2">
<path d="M21.698,10.658L24,12L11.998,19L0,12L2.301,10.658L11.998,16.316L21.698,10.658ZM11.998,21.315L2.301,15.657L0,17L11.998,24L24,17L21.698,15.658L11.998,21.315ZM24,7L11.998,-0L0,7L11.998,14L24,7Z" fill="#5C6F8A" fill-rule="nonzero"/>
</svg>

Before

Width:  |  Height:  |  Size: 704 B

After

Width:  |  Height:  |  Size: 690 B

+2 -2
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M24,13.616L24,10.384C22.349,9.797 21.306,9.632 20.781,8.365L20.781,8.364C20.254,7.093 20.881,6.23 21.628,4.657L19.343,2.372C17.782,3.114 16.91,3.747 15.636,3.219L15.635,3.219C14.366,2.693 14.2,1.643 13.616,0L10.384,0C9.802,1.635 9.635,2.692 8.365,3.219L8.364,3.219C7.093,3.747 6.232,3.121 4.657,2.372L2.372,4.657C3.117,6.225 3.747,7.091 3.219,8.364C2.692,9.635 1.635,9.802 0,10.384L0,13.616C1.632,14.196 2.692,14.365 3.219,15.635C3.749,16.917 3.105,17.801 2.372,19.342L4.657,21.628C6.219,20.885 7.091,20.253 8.364,20.781L8.365,20.781C9.635,21.307 9.801,22.36 10.384,24L13.616,24C14.198,22.364 14.366,21.31 15.643,20.778L15.644,20.778C16.906,20.254 17.764,20.879 19.342,21.629L21.627,19.343C20.883,17.78 20.252,16.91 20.779,15.637C21.306,14.366 22.367,14.197 24,13.616ZM12,16C9.791,16 8,14.209 8,12C8,9.791 9.791,8 12,8C14.209,8 16,9.791 16,12C16,14.209 14.209,16 12,16Z" style="fill:white;fill-rule:nonzero;"/>
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2">
<path d="M24,13.616L24,10.384C22.349,9.797 21.306,9.632 20.781,8.365L20.781,8.364C20.254,7.093 20.881,6.23 21.628,4.657L19.343,2.372C17.782,3.114 16.91,3.747 15.636,3.219L15.635,3.219C14.366,2.693 14.2,1.643 13.616,0L10.384,0C9.802,1.635 9.635,2.692 8.365,3.219L8.364,3.219C7.093,3.747 6.232,3.121 4.657,2.372L2.372,4.657C3.117,6.225 3.747,7.091 3.219,8.364C2.692,9.635 1.635,9.802 0,10.384L0,13.616C1.632,14.196 2.692,14.365 3.219,15.635C3.749,16.917 3.105,17.801 2.372,19.342L4.657,21.628C6.219,20.885 7.091,20.253 8.364,20.781L8.365,20.781C9.635,21.307 9.801,22.36 10.384,24L13.616,24C14.198,22.364 14.366,21.31 15.643,20.778L15.644,20.778C16.906,20.254 17.764,20.879 19.342,21.629L21.627,19.343C20.883,17.78 20.252,16.91 20.779,15.637C21.306,14.366 22.367,14.197 24,13.616ZM12,16C9.791,16 8,14.209 8,12C8,9.791 9.791,8 12,8C14.209,8 16,9.791 16,12C16,14.209 14.209,16 12,16Z" fill="white" fill-rule="nonzero"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

+4 -6
View File
@@ -1,7 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.0349343,0,0,0.0349343,-55.5414,-28.6416)">
<path d="M1878.62,1496.58C1719.5,1470.41 1597.95,1332.09 1597.95,1165.6C1597.95,980.47 1748.25,830.166 1933.38,830.166C2118.51,830.166 2268.82,980.47 2268.82,1165.6C2268.82,1331.86 2147.6,1470.03 1988.8,1496.47L1988.8,1466.79C2131.38,1440.68 2239.59,1315.68 2239.59,1165.6C2239.59,996.604 2102.38,859.399 1933.38,859.399C1764.39,859.399 1627.18,996.604 1627.18,1165.6C1627.18,1315.91 1735.72,1441.07 1878.62,1466.91L1878.62,1496.58ZM1878.62,1400.68C1771.77,1375.85 1692.06,1279.96 1692.06,1165.6C1692.06,1032.41 1800.19,924.274 1933.38,924.274C2066.57,924.274 2174.71,1032.41 2174.71,1165.6C2174.71,1279.73 2095.32,1375.46 1988.8,1400.53L1988.8,1370.37C2079.02,1345.98 2145.48,1263.49 2145.48,1165.6C2145.48,1048.54 2050.44,953.507 1933.38,953.507C1816.33,953.507 1721.29,1048.54 1721.29,1165.6C1721.29,1263.73 1788.07,1346.38 1878.62,1370.55L1878.62,1400.68ZM1878.62,1304.01C1823.55,1282.18 1784.56,1228.41 1784.56,1165.6C1784.56,1083.46 1851.24,1016.77 1933.38,1016.77C2015.52,1016.77 2082.21,1083.46 2082.21,1165.6C2082.21,1228.16 2043.53,1281.76 1988.8,1303.75L1988.8,1271.59C2026.93,1251.59 2052.98,1211.62 2052.98,1165.6C2052.98,1099.6 1999.39,1046.01 1933.38,1046.01C1867.38,1046.01 1813.79,1099.6 1813.79,1165.6C1813.79,1211.88 1840.13,1252.05 1878.62,1271.93L1878.62,1304.01ZM1933.38,1109.82C1964.17,1109.82 1989.16,1134.82 1989.16,1165.6C1989.16,1196.39 1964.17,1221.38 1933.38,1221.38C1902.6,1221.38 1877.6,1196.39 1877.6,1165.6C1877.6,1134.82 1902.6,1109.82 1933.38,1109.82Z" style="fill:rgb(92,111,138);"/>
</g>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<g transform="matrix(0.0349343 0 0 0.0349343 -55.5414 -28.6416)">
<path d="M1878.62 1496.58c-159.12-26.17-280.67-164.49-280.67-330.98 0-185.13 150.3-335.43 335.43-335.43s335.44 150.3 335.44 335.43c0 166.26-121.22 304.43-280.02 330.87v-29.68c142.58-26.11 250.79-151.11 250.8-301.19 0-169-137.21-306.21-306.21-306.21s-306.2 137.21-306.2 306.21c0 150.31 108.54 275.47 251.44 301.33v29.73Zm0-95.9c-106.85-24.83-186.56-120.72-186.56-235.08 0-133.19 108.13-241.33 241.32-241.33s241.33 108.14 241.33 241.33c0 113.13-79.39 208.86-185.91 233.93v-30.16c90.22-24.39 156.68-106.88 156.68-204.77 0-117.06-97.12-214.22-214.18-214.22s-214.18 97.12-214.18 214.22c0 97.13 66.78 179.78 157.33 203.95v32.08Zm0-96.67c-55.07-21.83-94.06-75.6-94.06-138.41 0-82.14 66.68-148.83 148.82-148.83s148.83 66.69 148.83 148.83c0 62.56-38.68 116.16-93.41 138.15v-32.16c38.13-20 64.18-59.97 64.18-106 0-66 53.59-119.59 119.6-119.59s119.59 53.59 119.59 119.59c0 46.28-26.34 86.45-64.83 106.33v32.08c-55.07 21.83-94.06 75.6-94.06 138.41 0 82.14 66.68 148.83 148.82 148.83s148.83-66.69 148.83-148.83c0-62.6-38.68-116.2-93.41-138.19v-32.08c38.13-20 64.18-59.97 64.18-106 0-66-53.59-119.59-119.6-119.59s-119.59 53.59-119.59 119.59c0 46.28 26.34 86.45 64.83 106.33v32.08c-55.07 21.83-94.06 75.6-94.06 138.41 0 82.14 66.68 148.83 148.82 148.83s148.83-66.69 148.83-148.83c0-62.56-38.68-116.16-93.41-138.15v-32.16c38.13-20 64.18-59.97 64.18-106 0-66-53.59-119.59-119.6-119.59s-119.59 53.59-119.59 119.59c0 46.28 26.34 86.45 64.83 106.33v32.08Z" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

+19 -27
View File
@@ -1,29 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 318.32 108.98">
<defs>
<style>
.cls-1 {
fill: #383838;
}
</style>
</defs>
<g>
<rect class="cls-1" x="7.1" y="7.8" width="5.84" height="5.48"/>
<rect class="cls-1" x="7.1" y="18.78" width="24.02" height="5.48"/>
<rect class="cls-1" x="7.1" y="29.77" width="42.85" height="5.48"/>
<rect class="cls-1" x="7.1" y="40.76" width="61.68" height="5.48"/>
<rect class="cls-1" x="7.1" y="51.75" width="80.88" height="5.48"/>
<rect class="cls-1" x="7.1" y="62.74" width="61.68" height="5.48"/>
<rect class="cls-1" x="7.1" y="73.73" width="42.85" height="5.48"/>
<rect class="cls-1" x="7.1" y="84.72" width="24.02" height="5.48"/>
<rect class="cls-1" x="7.1" y="95.71" width="5.84" height="5.48"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 318.32 108.98">
<g class="bv-logo__accent">
<rect x="7.1" y="7.8" width="5.84" height="5.48" />
<rect x="7.1" y="18.78" width="24.02" height="5.48" />
<rect x="7.1" y="29.77" width="42.85" height="5.48" />
<rect x="7.1" y="40.76" width="61.68" height="5.48" />
<rect x="7.1" y="51.75" width="80.88" height="5.48" />
<rect x="7.1" y="62.74" width="61.68" height="5.48" />
<rect x="7.1" y="73.73" width="42.85" height="5.48" />
<rect x="7.1" y="84.72" width="24.02" height="5.48" />
<rect x="7.1" y="95.71" width="5.84" height="5.48" />
</g>
<g>
<path class="cls-1" d="M121.28,25.08v44.54c0,5.67,2.79,8.54,8.22,8.54h11.57c5.51,0,8.3-2.87,8.3-8.54v-13.09c0-5.67-2.79-8.46-8.3-8.46h-16.6v-5.83h17.72c8.7,0,13.01,4.47,13.01,13.33v15.01c0,8.86-4.31,13.33-13.01,13.33h-13.73c-8.7,0-13.01-4.47-13.01-13.33V25.08h5.83Z"/>
<path class="cls-1" d="M168.53,25.08v6.26h-5.83v-6.26h5.83ZM168.53,42.64v41.27h-5.83v-41.27h5.83Z"/>
<path class="cls-1" d="M191.36,25.08v17.16h9.02v5.19h-9.02v36.48h-5.83v-36.48h-9.82v-5.19h9.82v-17.16h5.83Z"/>
<path class="cls-1" d="M212.88,42.24l16.84,35.68,16.6-35.68h6.07l-19.56,41.67h-6.31l-20.04-41.67h6.39Z"/>
<path class="cls-1" d="M264.05,25.08v6.26h-5.83v-6.26h5.83ZM264.05,42.24v41.67h-5.83v-41.67h5.83Z"/>
<path class="cls-1" d="M311.21,25.08v45.5c0,8.86-4.31,13.33-13.01,13.33h-13.73c-8.7,0-13.01-4.47-13.01-13.33v-15.01c0-8.86,4.31-13.33,13.01-13.33h17.72v5.83h-16.6c-5.51,0-8.3,2.79-8.3,8.46v13.09c0,5.67,2.79,8.54,8.3,8.54h11.49c5.51,0,8.3-2.87,8.3-8.54V25.08h5.83Z"/>
<g class="bv-logo__wordmark">
<path d="M121.28 25.08v44.54c0 5.67 2.79 8.54 8.22 8.54h11.57c5.51 0 8.3-2.87 8.3-8.54v-13.09c0-5.67-2.79-8.46-8.3-8.46h-16.6v-5.83h17.72c8.7 0 13.01 4.47 13.01 13.33v15.01c0 8.86-4.31 13.33-13.01 13.33h-13.73c-8.7 0-13.01-4.47-13.01-13.33V25.08h5.83Z" />
<path d="M168.53 25.08v6.26h-5.83v-6.26h5.83Zm0 17.56v41.27h-5.83v-41.27h5.83Z" />
<path d="M191.36 25.08v17.16h9.02v5.19h-9.02v36.48h-5.83v-36.48h-9.82v-5.19h9.82V25.08h5.83Z" />
<path d="M212.88 42.24l16.84 35.68 16.6-35.68h6.07l-19.56 41.67h-6.31l-20.04-41.67h6.39Z" />
<path d="M264.05 25.08v6.26h-5.83v-6.26h5.83Zm0 17.16v41.67h-5.83v-41.67h5.83Z" />
<path d="M311.21 25.08v45.5c0 8.86-4.31 13.33-13.01 13.33h-13.73c-8.7 0-13.01-4.47-13.01-13.33v-15.01c0-8.86 4.31-13.33 13.01-13.33h17.72v5.83h-16.6c-5.51 0-8.3 2.79-8.3 8.46v13.09c0 5.67 2.79 8.54 8.3 8.54h11.49c5.51 0 8.3-2.87 8.3-8.54V25.08h5.83Z" />
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

+19 -27
View File
@@ -1,29 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 318.32 108.98">
<defs>
<style>
.cls-1 {
fill: #fff;
}
</style>
</defs>
<g>
<rect class="cls-1" x="7.1" y="7.8" width="5.84" height="5.48"/>
<rect class="cls-1" x="7.1" y="18.78" width="24.02" height="5.48"/>
<rect class="cls-1" x="7.1" y="29.77" width="42.85" height="5.48"/>
<rect class="cls-1" x="7.1" y="40.76" width="61.68" height="5.48"/>
<rect class="cls-1" x="7.1" y="51.75" width="80.88" height="5.48"/>
<rect class="cls-1" x="7.1" y="62.74" width="61.68" height="5.48"/>
<rect class="cls-1" x="7.1" y="73.73" width="42.85" height="5.48"/>
<rect class="cls-1" x="7.1" y="84.72" width="24.02" height="5.48"/>
<rect class="cls-1" x="7.1" y="95.71" width="5.84" height="5.48"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 318.32 108.98">
<g class="bv-logo__accent">
<rect x="7.1" y="7.8" width="5.84" height="5.48" />
<rect x="7.1" y="18.78" width="24.02" height="5.48" />
<rect x="7.1" y="29.77" width="42.85" height="5.48" />
<rect x="7.1" y="40.76" width="61.68" height="5.48" />
<rect x="7.1" y="51.75" width="80.88" height="5.48" />
<rect x="7.1" y="62.74" width="61.68" height="5.48" />
<rect x="7.1" y="73.73" width="42.85" height="5.48" />
<rect x="7.1" y="84.72" width="24.02" height="5.48" />
<rect x="7.1" y="95.71" width="5.84" height="5.48" />
</g>
<g>
<path class="cls-1" d="M121.28,25.08v44.54c0,5.67,2.79,8.54,8.22,8.54h11.57c5.51,0,8.3-2.87,8.3-8.54v-13.09c0-5.67-2.79-8.46-8.3-8.46h-16.6v-5.83h17.72c8.7,0,13.01,4.47,13.01,13.33v15.01c0,8.86-4.31,13.33-13.01,13.33h-13.73c-8.7,0-13.01-4.47-13.01-13.33V25.08h5.83Z"/>
<path class="cls-1" d="M168.53,25.08v6.26h-5.83v-6.26h5.83ZM168.53,42.64v41.27h-5.83v-41.27h5.83Z"/>
<path class="cls-1" d="M191.36,25.08v17.16h9.02v5.19h-9.02v36.48h-5.83v-36.48h-9.82v-5.19h9.82v-17.16h5.83Z"/>
<path class="cls-1" d="M212.88,42.24l16.84,35.68,16.6-35.68h6.07l-19.56,41.67h-6.31l-20.04-41.67h6.39Z"/>
<path class="cls-1" d="M264.05,25.08v6.26h-5.83v-6.26h5.83ZM264.05,42.24v41.67h-5.83v-41.67h5.83Z"/>
<path class="cls-1" d="M311.21,25.08v45.5c0,8.86-4.31,13.33-13.01,13.33h-13.73c-8.7,0-13.01-4.47-13.01-13.33v-15.01c0-8.86,4.31-13.33,13.01-13.33h17.72v5.83h-16.6c-5.51,0-8.3,2.79-8.3,8.46v13.09c0,5.67,2.79,8.54,8.3,8.54h11.49c5.51,0,8.3-2.87,8.3-8.54V25.08h5.83Z"/>
<g class="bv-logo__wordmark">
<path d="M121.28 25.08v44.54c0 5.67 2.79 8.54 8.22 8.54h11.57c5.51 0 8.3-2.87 8.3-8.54v-13.09c0-5.67-2.79-8.46-8.3-8.46h-16.6v-5.83h17.72c8.7 0 13.01 4.47 13.01 13.33v15.01c0 8.86-4.31 13.33-13.01 13.33h-13.73c-8.7 0-13.01-4.47-13.01-13.33V25.08h5.83Z" />
<path d="M168.53 25.08v6.26h-5.83v-6.26h5.83Zm0 17.56v41.27h-5.83v-41.27h5.83Z" />
<path d="M191.36 25.08v17.16h9.02v5.19h-9.02v36.48h-5.83v-36.48h-9.82v-5.19h9.82V25.08h5.83Z" />
<path d="M212.88 42.24l16.84 35.68 16.6-35.68h6.07l-19.56 41.67h-6.31l-20.04-41.67h6.39Z" />
<path d="M264.05 25.08v6.26h-5.83v-6.26h5.83Zm0 17.16v41.67h-5.83v-41.67h5.83Z" />
<path d="M311.21 25.08v45.5c0 8.86-4.31 13.33-13.01 13.33h-13.73c-8.7 0-13.01-4.47-13.01-13.33v-15.01c0-8.86 4.31-13.33 13.01-13.33h17.72v5.83h-16.6c-5.51 0-8.3 2.79-8.3 8.46v13.09c0 5.67 2.79 8.54 8.3 8.54h11.49c5.51 0 8.3-2.87 8.3-8.54V25.08h5.83Z" />
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

+19 -31
View File
@@ -1,33 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 318.32 108.98">
<defs>
<style>
.cls-1 {
fill: #fe0032;
}
.cls-2 {
fill: #383838;
}
</style>
</defs>
<g>
<rect class="cls-1" x="7.1" y="7.8" width="5.84" height="5.48"/>
<rect class="cls-1" x="7.1" y="18.78" width="24.02" height="5.48"/>
<rect class="cls-1" x="7.1" y="29.77" width="42.85" height="5.48"/>
<rect class="cls-1" x="7.1" y="40.76" width="61.68" height="5.48"/>
<rect class="cls-1" x="7.1" y="51.75" width="80.88" height="5.48"/>
<rect class="cls-1" x="7.1" y="62.74" width="61.68" height="5.48"/>
<rect class="cls-1" x="7.1" y="73.73" width="42.85" height="5.48"/>
<rect class="cls-1" x="7.1" y="84.72" width="24.02" height="5.48"/>
<rect class="cls-1" x="7.1" y="95.71" width="5.84" height="5.48"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 318.32 108.98">
<g class="bv-logo__accent">
<rect x="7.1" y="7.8" width="5.84" height="5.48" />
<rect x="7.1" y="18.78" width="24.02" height="5.48" />
<rect x="7.1" y="29.77" width="42.85" height="5.48" />
<rect x="7.1" y="40.76" width="61.68" height="5.48" />
<rect x="7.1" y="51.75" width="80.88" height="5.48" />
<rect x="7.1" y="62.74" width="61.68" height="5.48" />
<rect x="7.1" y="73.73" width="42.85" height="5.48" />
<rect x="7.1" y="84.72" width="24.02" height="5.48" />
<rect x="7.1" y="95.71" width="5.84" height="5.48" />
</g>
<g>
<path class="cls-2" d="M121.28,25.08v44.54c0,5.67,2.79,8.54,8.22,8.54h11.57c5.51,0,8.3-2.87,8.3-8.54v-13.09c0-5.67-2.79-8.46-8.3-8.46h-16.6v-5.83h17.72c8.7,0,13.01,4.47,13.01,13.33v15.01c0,8.86-4.31,13.33-13.01,13.33h-13.73c-8.7,0-13.01-4.47-13.01-13.33V25.08h5.83Z"/>
<path class="cls-2" d="M168.53,25.08v6.26h-5.83v-6.26h5.83ZM168.53,42.64v41.27h-5.83v-41.27h5.83Z"/>
<path class="cls-2" d="M191.36,25.08v17.16h9.02v5.19h-9.02v36.48h-5.83v-36.48h-9.82v-5.19h9.82v-17.16h5.83Z"/>
<path class="cls-2" d="M212.88,42.24l16.84,35.68,16.6-35.68h6.07l-19.56,41.67h-6.31l-20.04-41.67h6.39Z"/>
<path class="cls-2" d="M264.05,25.08v6.26h-5.83v-6.26h5.83ZM264.05,42.24v41.67h-5.83v-41.67h5.83Z"/>
<path class="cls-2" d="M311.21,25.08v45.5c0,8.86-4.31,13.33-13.01,13.33h-13.73c-8.7,0-13.01-4.47-13.01-13.33v-15.01c0-8.86,4.31-13.33,13.01-13.33h17.72v5.83h-16.6c-5.51,0-8.3,2.79-8.3,8.46v13.09c0,5.67,2.79,8.54,8.3,8.54h11.49c5.51,0,8.3-2.87,8.3-8.54V25.08h5.83Z"/>
<g class="bv-logo__wordmark">
<path d="M121.28 25.08v44.54c0 5.67 2.79 8.54 8.22 8.54h11.57c5.51 0 8.3-2.87 8.3-8.54v-13.09c0-5.67-2.79-8.46-8.3-8.46h-16.6v-5.83h17.72c8.7 0 13.01 4.47 13.01 13.33v15.01c0 8.86-4.31 13.33-13.01 13.33h-13.73c-8.7 0-13.01-4.47-13.01-13.33V25.08h5.83Z" />
<path d="M168.53 25.08v6.26h-5.83v-6.26h5.83Zm0 17.56v41.27h-5.83v-41.27h5.83Z" />
<path d="M191.36 25.08v17.16h9.02v5.19h-9.02v36.48h-5.83v-36.48h-9.82v-5.19h9.82V25.08h5.83Z" />
<path d="M212.88 42.24l16.84 35.68 16.6-35.68h6.07l-19.56 41.67h-6.31l-20.04-41.67h6.39Z" />
<path d="M264.05 25.08v6.26h-5.83v-6.26h5.83Zm0 17.16v41.67h-5.83v-41.67h5.83Z" />
<path d="M311.21 25.08v45.5c0 8.86-4.31 13.33-13.01 13.33h-13.73c-8.7 0-13.01-4.47-13.01-13.33v-15.01c0-8.86 4.31-13.33 13.01-13.33h17.72v5.83h-16.6c-5.51 0-8.3 2.79-8.3 8.46v13.09c0 5.67 2.79 8.54 8.3 8.54h11.49c5.51 0 8.3-2.87 8.3-8.54V25.08h5.83Z" />
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

+19 -31
View File
@@ -1,33 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 318.32 108.98">
<defs>
<style>
.cls-1 {
fill: #fe0032;
}
.cls-2 {
fill: #fff;
}
</style>
</defs>
<g>
<rect class="cls-1" x="7.1" y="7.8" width="5.84" height="5.48"/>
<rect class="cls-1" x="7.1" y="18.78" width="24.02" height="5.48"/>
<rect class="cls-1" x="7.1" y="29.77" width="42.85" height="5.48"/>
<rect class="cls-1" x="7.1" y="40.76" width="61.68" height="5.48"/>
<rect class="cls-1" x="7.1" y="51.75" width="80.88" height="5.48"/>
<rect class="cls-1" x="7.1" y="62.74" width="61.68" height="5.48"/>
<rect class="cls-1" x="7.1" y="73.73" width="42.85" height="5.48"/>
<rect class="cls-1" x="7.1" y="84.72" width="24.02" height="5.48"/>
<rect class="cls-1" x="7.1" y="95.71" width="5.84" height="5.48"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 318.32 108.98">
<g class="bv-logo__accent">
<rect x="7.1" y="7.8" width="5.84" height="5.48" />
<rect x="7.1" y="18.78" width="24.02" height="5.48" />
<rect x="7.1" y="29.77" width="42.85" height="5.48" />
<rect x="7.1" y="40.76" width="61.68" height="5.48" />
<rect x="7.1" y="51.75" width="80.88" height="5.48" />
<rect x="7.1" y="62.74" width="61.68" height="5.48" />
<rect x="7.1" y="73.73" width="42.85" height="5.48" />
<rect x="7.1" y="84.72" width="24.02" height="5.48" />
<rect x="7.1" y="95.71" width="5.84" height="5.48" />
</g>
<g>
<path class="cls-2" d="M121.28,25.08v44.54c0,5.67,2.79,8.54,8.22,8.54h11.57c5.51,0,8.3-2.87,8.3-8.54v-13.09c0-5.67-2.79-8.46-8.3-8.46h-16.6v-5.83h17.72c8.7,0,13.01,4.47,13.01,13.33v15.01c0,8.86-4.31,13.33-13.01,13.33h-13.73c-8.7,0-13.01-4.47-13.01-13.33V25.08h5.83Z"/>
<path class="cls-2" d="M168.53,25.08v6.26h-5.83v-6.26h5.83ZM168.53,42.64v41.27h-5.83v-41.27h5.83Z"/>
<path class="cls-2" d="M191.36,25.08v17.16h9.02v5.19h-9.02v36.48h-5.83v-36.48h-9.82v-5.19h9.82v-17.16h5.83Z"/>
<path class="cls-2" d="M212.88,42.24l16.84,35.68,16.6-35.68h6.07l-19.56,41.67h-6.31l-20.04-41.67h6.39Z"/>
<path class="cls-2" d="M264.05,25.08v6.26h-5.83v-6.26h5.83ZM264.05,42.24v41.67h-5.83v-41.67h5.83Z"/>
<path class="cls-2" d="M311.21,25.08v45.5c0,8.86-4.31,13.33-13.01,13.33h-13.73c-8.7,0-13.01-4.47-13.01-13.33v-15.01c0-8.86,4.31-13.33,13.01-13.33h17.72v5.83h-16.6c-5.51,0-8.3,2.79-8.3,8.46v13.09c0,5.67,2.79,8.54,8.3,8.54h11.49c5.51,0,8.3-2.87,8.3-8.54V25.08h5.83Z"/>
<g class="bv-logo__wordmark">
<path d="M121.28 25.08v44.54c0 5.67 2.79 8.54 8.22 8.54h11.57c5.51 0 8.3-2.87 8.3-8.54v-13.09c0-5.67-2.79-8.46-8.3-8.46h-16.6v-5.83h17.72c8.7 0 13.01 4.47 13.01 13.33v15.01c0 8.86-4.31 13.33-13.01 13.33h-13.73c-8.7 0-13.01-4.47-13.01-13.33V25.08h5.83Z" />
<path d="M168.53 25.08v6.26h-5.83v-6.26h5.83Zm0 17.56v41.27h-5.83v-41.27h5.83Z" />
<path d="M191.36 25.08v17.16h9.02v5.19h-9.02v36.48h-5.83v-36.48h-9.82v-5.19h9.82V25.08h5.83Z" />
<path d="M212.88 42.24l16.84 35.68 16.6-35.68h6.07l-19.56 41.67h-6.31l-20.04-41.67h6.39Z" />
<path d="M264.05 25.08v6.26h-5.83v-6.26h5.83Zm0 17.16v41.67h-5.83v-41.67h5.83Z" />
<path d="M311.21 25.08v45.5c0 8.86-4.31 13.33-13.01 13.33h-13.73c-8.7 0-13.01-4.47-13.01-13.33v-15.01c0-8.86 4.31-13.33 13.01-13.33h17.72v5.83h-16.6c-5.51 0-8.3 2.79-8.3 8.46v13.09c0 5.67 2.79 8.54 8.3 8.54h11.49c5.51 0 8.3-2.87 8.3-8.54V25.08h5.83Z" />
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

+13 -19
View File
@@ -1,19 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 112.46 112.46">
<defs>
<style>
.cls-1 {
fill: #fff;
}
</style>
</defs>
<rect class="cls-1" x="15.79" y="9.53" width="5.84" height="5.48"/>
<rect class="cls-1" x="15.79" y="20.52" width="24.02" height="5.48"/>
<rect class="cls-1" x="15.79" y="31.51" width="42.85" height="5.48"/>
<rect class="cls-1" x="15.79" y="42.5" width="61.68" height="5.48"/>
<rect class="cls-1" x="15.79" y="53.49" width="80.88" height="5.48"/>
<rect class="cls-1" x="15.79" y="64.48" width="61.68" height="5.48"/>
<rect class="cls-1" x="15.79" y="75.47" width="42.85" height="5.48"/>
<rect class="cls-1" x="15.79" y="86.45" width="24.02" height="5.48"/>
<rect class="cls-1" x="15.79" y="97.44" width="5.84" height="5.48"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 112.46 112.46">
<g class="bv-logo__accent">
<rect x="15.79" y="9.53" width="5.84" height="5.48" />
<rect x="15.79" y="20.52" width="24.02" height="5.48" />
<rect x="15.79" y="31.51" width="42.85" height="5.48" />
<rect x="15.79" y="42.5" width="61.68" height="5.48" />
<rect x="15.79" y="53.49" width="80.88" height="5.48" />
<rect x="15.79" y="64.48" width="61.68" height="5.48" />
<rect x="15.79" y="75.47" width="42.85" height="5.48" />
<rect x="15.79" y="86.45" width="24.02" height="5.48" />
<rect x="15.79" y="97.44" width="5.84" height="5.48" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 878 B

After

Width:  |  Height:  |  Size: 658 B

+13 -19
View File
@@ -1,19 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 112.46 112.46">
<defs>
<style>
.cls-1 {
fill: #383838;
}
</style>
</defs>
<rect class="cls-1" x="15.79" y="9.53" width="5.84" height="5.48"/>
<rect class="cls-1" x="15.79" y="20.52" width="24.02" height="5.48"/>
<rect class="cls-1" x="15.79" y="31.51" width="42.85" height="5.48"/>
<rect class="cls-1" x="15.79" y="42.5" width="61.68" height="5.48"/>
<rect class="cls-1" x="15.79" y="53.49" width="80.88" height="5.48"/>
<rect class="cls-1" x="15.79" y="64.48" width="61.68" height="5.48"/>
<rect class="cls-1" x="15.79" y="75.47" width="42.85" height="5.48"/>
<rect class="cls-1" x="15.79" y="86.45" width="24.02" height="5.48"/>
<rect class="cls-1" x="15.79" y="97.44" width="5.84" height="5.48"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 112.46 112.46">
<g class="bv-logo__accent">
<rect x="15.79" y="9.53" width="5.84" height="5.48" />
<rect x="15.79" y="20.52" width="24.02" height="5.48" />
<rect x="15.79" y="31.51" width="42.85" height="5.48" />
<rect x="15.79" y="42.5" width="61.68" height="5.48" />
<rect x="15.79" y="53.49" width="80.88" height="5.48" />
<rect x="15.79" y="64.48" width="61.68" height="5.48" />
<rect x="15.79" y="75.47" width="42.85" height="5.48" />
<rect x="15.79" y="86.45" width="24.02" height="5.48" />
<rect x="15.79" y="97.44" width="5.84" height="5.48" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 881 B

After

Width:  |  Height:  |  Size: 658 B

+13 -19
View File
@@ -1,19 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 112.46 112.46">
<defs>
<style>
.cls-1 {
fill: #fe0032;
}
</style>
</defs>
<rect class="cls-1" x="15.79" y="9.53" width="5.84" height="5.48"/>
<rect class="cls-1" x="15.79" y="20.52" width="24.02" height="5.48"/>
<rect class="cls-1" x="15.79" y="31.51" width="42.85" height="5.48"/>
<rect class="cls-1" x="15.79" y="42.5" width="61.68" height="5.48"/>
<rect class="cls-1" x="15.79" y="53.49" width="80.88" height="5.48"/>
<rect class="cls-1" x="15.79" y="64.48" width="61.68" height="5.48"/>
<rect class="cls-1" x="15.79" y="75.47" width="42.85" height="5.48"/>
<rect class="cls-1" x="15.79" y="86.45" width="24.02" height="5.48"/>
<rect class="cls-1" x="15.79" y="97.44" width="5.84" height="5.48"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 112.46 112.46">
<g class="bv-logo__accent">
<rect x="15.79" y="9.53" width="5.84" height="5.48" />
<rect x="15.79" y="20.52" width="24.02" height="5.48" />
<rect x="15.79" y="31.51" width="42.85" height="5.48" />
<rect x="15.79" y="42.5" width="61.68" height="5.48" />
<rect x="15.79" y="53.49" width="80.88" height="5.48" />
<rect x="15.79" y="64.48" width="61.68" height="5.48" />
<rect x="15.79" y="75.47" width="42.85" height="5.48" />
<rect x="15.79" y="86.45" width="24.02" height="5.48" />
<rect x="15.79" y="97.44" width="5.84" height="5.48" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 881 B

After

Width:  |  Height:  |  Size: 658 B

+10 -16
View File
@@ -1,16 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 229.82 88">
<defs>
<style>
.cls-1 {
fill: #383838;
}
</style>
</defs>
<path class="cls-1" d="M22.86,14.59v44.54c0,5.67,2.79,8.54,8.22,8.54h11.57c5.51,0,8.3-2.87,8.3-8.54v-13.09c0-5.67-2.79-8.46-8.3-8.46h-16.6v-5.83h17.72c8.7,0,13.01,4.47,13.01,13.33v15.01c0,8.86-4.31,13.33-13.01,13.33h-13.73c-8.7,0-13.01-4.47-13.01-13.33V14.59h5.83Z"/>
<path class="cls-1" d="M70.11,14.59v6.26h-5.83v-6.26h5.83ZM70.11,32.15v41.27h-5.83v-41.27h5.83Z"/>
<path class="cls-1" d="M92.94,14.59v17.16h9.02v5.19h-9.02v36.48h-5.83v-36.48h-9.82v-5.19h9.82V14.59h5.83Z"/>
<path class="cls-1" d="M114.46,31.75l16.84,35.68,16.6-35.68h6.07l-19.56,41.67h-6.31l-20.04-41.67h6.39Z"/>
<path class="cls-1" d="M165.63,14.59v6.26h-5.83v-6.26h5.83ZM165.63,31.75v41.67h-5.83V31.75h5.83Z"/>
<path class="cls-1" d="M212.79,14.59v45.5c0,8.86-4.31,13.33-13.01,13.33h-13.73c-8.7,0-13.01-4.47-13.01-13.33v-15.01c0-8.86,4.31-13.33,13.01-13.33h17.72v5.83h-16.6c-5.51,0-8.3,2.79-8.3,8.46v13.09c0,5.67,2.79,8.54,8.3,8.54h11.49c5.51,0,8.3-2.87,8.3-8.54V14.59h5.83Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 229.82 88">
<g class="bv-logo__wordmark">
<path d="M22.86 14.59v44.54c0 5.67 2.79 8.54 8.22 8.54h11.57c5.51 0 8.3-2.87 8.3-8.54v-13.09c0-5.67-2.79-8.46-8.3-8.46h-16.6v-5.83h17.72c8.7 0 13.01 4.47 13.01 13.33v15.01c0 8.86-4.31 13.33-13.01 13.33h-13.73c-8.7 0-13.01-4.47-13.01-13.33V14.59h5.83Z" />
<path d="M70.11 14.59v6.26h-5.83v-6.26h5.83Zm0 17.56v41.27h-5.83v-41.27h5.83Z" />
<path d="M92.94 14.59v17.16h9.02v5.19h-9.02v36.48h-5.83v-36.48h-9.82v-5.19h9.82V14.59h5.83Z" />
<path d="M114.46 31.75l16.84 35.68 16.6-35.68h6.07l-19.56 41.67h-6.31l-20.04-41.67h6.39Z" />
<path d="M165.63 14.59v6.26h-5.83v-6.26h5.83Zm0 17.16v41.67h-5.83V31.75h5.83Z" />
<path d="M212.79 14.59v45.5c0 8.86-4.31 13.33-13.01 13.33h-13.73c-8.7 0-13.01-4.47-13.01-13.33v-15.01c0-8.86 4.31-13.33 13.01-13.33h17.72v5.83h-16.6c-5.51 0-8.3 2.79-8.3 8.46v13.09c0 5.67 2.79 8.54 8.3 8.54h11.49c5.51 0 8.3-2.87 8.3-8.54V14.59h5.83Z" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 997 B

+10 -16
View File
@@ -1,16 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 229.82 88">
<defs>
<style>
.cls-1 {
fill: #fe0032;
}
</style>
</defs>
<path class="cls-1" d="M22.86,14.59v44.54c0,5.67,2.79,8.54,8.22,8.54h11.57c5.51,0,8.3-2.87,8.3-8.54v-13.09c0-5.67-2.79-8.46-8.3-8.46h-16.6v-5.83h17.72c8.7,0,13.01,4.47,13.01,13.33v15.01c0,8.86-4.31,13.33-13.01,13.33h-13.73c-8.7,0-13.01-4.47-13.01-13.33V14.59h5.83Z"/>
<path class="cls-1" d="M70.11,14.59v6.26h-5.83v-6.26h5.83ZM70.11,32.15v41.27h-5.83v-41.27h5.83Z"/>
<path class="cls-1" d="M92.94,14.59v17.16h9.02v5.19h-9.02v36.48h-5.83v-36.48h-9.82v-5.19h9.82V14.59h5.83Z"/>
<path class="cls-1" d="M114.46,31.75l16.84,35.68,16.6-35.68h6.07l-19.56,41.67h-6.31l-20.04-41.67h6.39Z"/>
<path class="cls-1" d="M165.63,14.59v6.26h-5.83v-6.26h5.83ZM165.63,31.75v41.67h-5.83V31.75h5.83Z"/>
<path class="cls-1" d="M212.79,14.59v45.5c0,8.86-4.31,13.33-13.01,13.33h-13.73c-8.7,0-13.01-4.47-13.01-13.33v-15.01c0-8.86,4.31-13.33,13.01-13.33h17.72v5.83h-16.6c-5.51,0-8.3,2.79-8.3,8.46v13.09c0,5.67,2.79,8.54,8.3,8.54h11.49c5.51,0,8.3-2.87,8.3-8.54V14.59h5.83Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 229.82 88">
<g class="bv-logo__wordmark">
<path d="M22.86 14.59v44.54c0 5.67 2.79 8.54 8.22 8.54h11.57c5.51 0 8.3-2.87 8.3-8.54v-13.09c0-5.67-2.79-8.46-8.3-8.46h-16.6v-5.83h17.72c8.7 0 13.01 4.47 13.01 13.33v15.01c0 8.86-4.31 13.33-13.01 13.33h-13.73c-8.7 0-13.01-4.47-13.01-13.33V14.59h5.83Z" />
<path d="M70.11 14.59v6.26h-5.83v-6.26h5.83Zm0 17.56v41.27h-5.83v-41.27h5.83Z" />
<path d="M92.94 14.59v17.16h9.02v5.19h-9.02v36.48h-5.83v-36.48h-9.82v-5.19h9.82V14.59h5.83Z" />
<path d="M114.46 31.75l16.84 35.68 16.6-35.68h6.07l-19.56 41.67h-6.31l-20.04-41.67h6.39Z" />
<path d="M165.63 14.59v6.26h-5.83v-6.26h5.83Zm0 17.16v41.67h-5.83V31.75h5.83Z" />
<path d="M212.79 14.59v45.5c0 8.86-4.31 13.33-13.01 13.33h-13.73c-8.7 0-13.01-4.47-13.01-13.33v-15.01c0-8.86 4.31-13.33 13.01-13.33h17.72v5.83h-16.6c-5.51 0-8.3 2.79-8.3 8.46v13.09c0 5.67 2.79 8.54 8.3 8.54h11.49c5.51 0 8.3-2.87 8.3-8.54V14.59h5.83Z" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 997 B

+10 -16
View File
@@ -1,16 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 229.82 88">
<defs>
<style>
.cls-1 {
fill: #fff;
}
</style>
</defs>
<path class="cls-1" d="M22.86,14.59v44.54c0,5.67,2.79,8.54,8.22,8.54h11.57c5.51,0,8.3-2.87,8.3-8.54v-13.09c0-5.67-2.79-8.46-8.3-8.46h-16.6v-5.83h17.72c8.7,0,13.01,4.47,13.01,13.33v15.01c0,8.86-4.31,13.33-13.01,13.33h-13.73c-8.7,0-13.01-4.47-13.01-13.33V14.59h5.83Z"/>
<path class="cls-1" d="M70.11,14.59v6.26h-5.83v-6.26h5.83ZM70.11,32.15v41.27h-5.83v-41.27h5.83Z"/>
<path class="cls-1" d="M92.94,14.59v17.16h9.02v5.19h-9.02v36.48h-5.83v-36.48h-9.82v-5.19h9.82V14.59h5.83Z"/>
<path class="cls-1" d="M114.46,31.75l16.84,35.68,16.6-35.68h6.07l-19.56,41.67h-6.31l-20.04-41.67h6.39Z"/>
<path class="cls-1" d="M165.63,14.59v6.26h-5.83v-6.26h5.83ZM165.63,31.75v41.67h-5.83V31.75h5.83Z"/>
<path class="cls-1" d="M212.79,14.59v45.5c0,8.86-4.31,13.33-13.01,13.33h-13.73c-8.7,0-13.01-4.47-13.01-13.33v-15.01c0-8.86,4.31-13.33,13.01-13.33h17.72v5.83h-16.6c-5.51,0-8.3,2.79-8.3,8.46v13.09c0,5.67,2.79,8.54,8.3,8.54h11.49c5.51,0,8.3-2.87,8.3-8.54V14.59h5.83Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 229.82 88">
<g class="bv-logo__wordmark">
<path d="M22.86 14.59v44.54c0 5.67 2.79 8.54 8.22 8.54h11.57c5.51 0 8.3-2.87 8.3-8.54v-13.09c0-5.67-2.79-8.46-8.3-8.46h-16.6v-5.83h17.72c8.7 0 13.01 4.47 13.01 13.33v15.01c0 8.86-4.31 13.33-13.01 13.33h-13.73c-8.7 0-13.01-4.47-13.01-13.33V14.59h5.83Z" />
<path d="M70.11 14.59v6.26h-5.83v-6.26h5.83Zm0 17.56v41.27h-5.83v-41.27h5.83Z" />
<path d="M92.94 14.59v17.16h9.02v5.19h-9.02v36.48h-5.83v-36.48h-9.82v-5.19h9.82V14.59h5.83Z" />
<path d="M114.46 31.75l16.84 35.68 16.6-35.68h6.07l-19.56 41.67h-6.31l-20.04-41.67h6.39Z" />
<path d="M165.63 14.59v6.26h-5.83v-6.26h5.83Zm0 17.16v41.67h-5.83V31.75h5.83Z" />
<path d="M212.79 14.59v45.5c0 8.86-4.31 13.33-13.01 13.33h-13.73c-8.7 0-13.01-4.47-13.01-13.33v-15.01c0-8.86 4.31-13.33 13.01-13.33h17.72v5.83h-16.6c-5.51 0-8.3 2.79-8.3 8.46v13.09c0 5.67 2.79 8.54 8.3 8.54h11.49c5.51 0 8.3-2.87 8.3-8.54V14.59h5.83Z" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 997 B

+13 -25
View File
@@ -1,26 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 196.27 196.27">
<defs>
<style>
.cls-1 {
fill: #fe0032;
}
.cls-2 {
fill: #383838;
}
</style>
</defs>
<rect class="cls-2" width="196.27" height="196.27"/>
<g>
<rect class="cls-1" x="67.7" y="51.44" width="5.84" height="5.48"/>
<rect class="cls-1" x="67.7" y="62.43" width="24.02" height="5.48"/>
<rect class="cls-1" x="67.7" y="73.41" width="42.85" height="5.48"/>
<rect class="cls-1" x="67.7" y="84.4" width="61.68" height="5.48"/>
<rect class="cls-1" x="67.7" y="95.39" width="80.88" height="5.48"/>
<rect class="cls-1" x="67.7" y="106.38" width="61.68" height="5.48"/>
<rect class="cls-1" x="67.7" y="117.37" width="42.85" height="5.48"/>
<rect class="cls-1" x="67.7" y="128.36" width="24.02" height="5.48"/>
<rect class="cls-1" x="67.7" y="139.35" width="5.84" height="5.48"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 196.27 196.27">
<rect class="bv-logo__background" width="196.27" height="196.27" />
<g class="bv-logo__accent">
<rect x="67.7" y="51.44" width="5.84" height="5.48" />
<rect x="67.7" y="62.43" width="24.02" height="5.48" />
<rect x="67.7" y="73.41" width="42.85" height="5.48" />
<rect x="67.7" y="84.4" width="61.68" height="5.48" />
<rect x="67.7" y="95.39" width="80.88" height="5.48" />
<rect x="67.7" y="106.38" width="61.68" height="5.48" />
<rect x="67.7" y="117.36" width="42.85" height="5.48" />
<rect x="67.7" y="128.35" width="24.02" height="5.48" />
<rect x="67.7" y="139.34" width="5.84" height="5.48" />
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 1010 B

After

Width:  |  Height:  |  Size: 724 B

+13 -25
View File
@@ -1,26 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 196.27 196.27">
<defs>
<style>
.cls-1 {
fill: #fff;
}
.cls-2 {
fill: #383838;
}
</style>
</defs>
<rect class="cls-2" width="196.27" height="196.27"/>
<g>
<rect class="cls-1" x="67.7" y="51.44" width="5.84" height="5.48"/>
<rect class="cls-1" x="67.7" y="62.43" width="24.02" height="5.48"/>
<rect class="cls-1" x="67.7" y="73.41" width="42.85" height="5.48"/>
<rect class="cls-1" x="67.7" y="84.4" width="61.68" height="5.48"/>
<rect class="cls-1" x="67.7" y="95.39" width="80.88" height="5.48"/>
<rect class="cls-1" x="67.7" y="106.38" width="61.68" height="5.48"/>
<rect class="cls-1" x="67.7" y="117.37" width="42.85" height="5.48"/>
<rect class="cls-1" x="67.7" y="128.36" width="24.02" height="5.48"/>
<rect class="cls-1" x="67.7" y="139.35" width="5.84" height="5.48"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 196.27 196.27">
<rect class="bv-logo__background" width="196.27" height="196.27" />
<g class="bv-logo__accent">
<rect x="67.7" y="51.44" width="5.84" height="5.48" />
<rect x="67.7" y="62.43" width="24.02" height="5.48" />
<rect x="67.7" y="73.41" width="42.85" height="5.48" />
<rect x="67.7" y="84.4" width="61.68" height="5.48" />
<rect x="67.7" y="95.39" width="80.88" height="5.48" />
<rect x="67.7" y="106.38" width="61.68" height="5.48" />
<rect x="67.7" y="117.36" width="42.85" height="5.48" />
<rect x="67.7" y="128.35" width="24.02" height="5.48" />
<rect x="67.7" y="139.34" width="5.84" height="5.48" />
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 1007 B

After

Width:  |  Height:  |  Size: 724 B

+13 -25
View File
@@ -1,26 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 196.27 196.27">
<defs>
<style>
.cls-1 {
fill: #fe0032;
}
.cls-2 {
fill: #383838;
}
</style>
</defs>
<rect class="cls-1" width="196.27" height="196.27"/>
<g>
<rect class="cls-2" x="67.7" y="51.44" width="5.84" height="5.48"/>
<rect class="cls-2" x="67.7" y="62.43" width="24.02" height="5.48"/>
<rect class="cls-2" x="67.7" y="73.41" width="42.85" height="5.48"/>
<rect class="cls-2" x="67.7" y="84.4" width="61.68" height="5.48"/>
<rect class="cls-2" x="67.7" y="95.39" width="80.88" height="5.48"/>
<rect class="cls-2" x="67.7" y="106.38" width="61.68" height="5.48"/>
<rect class="cls-2" x="67.7" y="117.37" width="42.85" height="5.48"/>
<rect class="cls-2" x="67.7" y="128.36" width="24.02" height="5.48"/>
<rect class="cls-2" x="67.7" y="139.35" width="5.84" height="5.48"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 196.27 196.27">
<rect class="bv-logo__background" width="196.27" height="196.27" />
<g class="bv-logo__wordmark">
<rect x="67.7" y="51.44" width="5.84" height="5.48" />
<rect x="67.7" y="62.43" width="24.02" height="5.48" />
<rect x="67.7" y="73.41" width="42.85" height="5.48" />
<rect x="67.7" y="84.4" width="61.68" height="5.48" />
<rect x="67.7" y="95.39" width="80.88" height="5.48" />
<rect x="67.7" y="106.38" width="61.68" height="5.48" />
<rect x="67.7" y="117.36" width="42.85" height="5.48" />
<rect x="67.7" y="128.35" width="24.02" height="5.48" />
<rect x="67.7" y="139.34" width="5.84" height="5.48" />
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 1010 B

After

Width:  |  Height:  |  Size: 726 B

+13 -25
View File
@@ -1,26 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 196.27 196.27">
<defs>
<style>
.cls-1 {
fill: #fe0032;
}
.cls-2 {
fill: #fff;
}
</style>
</defs>
<rect class="cls-1" width="196.27" height="196.27"/>
<g>
<rect class="cls-2" x="67.7" y="51.44" width="5.84" height="5.48"/>
<rect class="cls-2" x="67.7" y="62.43" width="24.02" height="5.48"/>
<rect class="cls-2" x="67.7" y="73.41" width="42.85" height="5.48"/>
<rect class="cls-2" x="67.7" y="84.4" width="61.68" height="5.48"/>
<rect class="cls-2" x="67.7" y="95.39" width="80.88" height="5.48"/>
<rect class="cls-2" x="67.7" y="106.38" width="61.68" height="5.48"/>
<rect class="cls-2" x="67.7" y="117.37" width="42.85" height="5.48"/>
<rect class="cls-2" x="67.7" y="128.36" width="24.02" height="5.48"/>
<rect class="cls-2" x="67.7" y="139.35" width="5.84" height="5.48"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 196.27 196.27">
<rect class="bv-logo__background" width="196.27" height="196.27" />
<g class="bv-logo__wordmark">
<rect x="67.7" y="51.44" width="5.84" height="5.48" />
<rect x="67.7" y="62.43" width="24.02" height="5.48" />
<rect x="67.7" y="73.41" width="42.85" height="5.48" />
<rect x="67.7" y="84.4" width="61.68" height="5.48" />
<rect x="67.7" y="95.39" width="80.88" height="5.48" />
<rect x="67.7" y="106.38" width="61.68" height="5.48" />
<rect x="67.7" y="117.36" width="42.85" height="5.48" />
<rect x="67.7" y="128.35" width="24.02" height="5.48" />
<rect x="67.7" y="139.34" width="5.84" height="5.48" />
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 1007 B

After

Width:  |  Height:  |  Size: 726 B

+13 -25
View File
@@ -1,26 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 196.27 196.27">
<defs>
<style>
.cls-1 {
fill: #fff;
}
.cls-2 {
fill: #383838;
}
</style>
</defs>
<rect class="cls-1" width="196.27" height="196.27"/>
<g>
<rect class="cls-2" x="67.7" y="51.44" width="5.84" height="5.48"/>
<rect class="cls-2" x="67.7" y="62.43" width="24.02" height="5.48"/>
<rect class="cls-2" x="67.7" y="73.41" width="42.85" height="5.48"/>
<rect class="cls-2" x="67.7" y="84.4" width="61.68" height="5.48"/>
<rect class="cls-2" x="67.7" y="95.39" width="80.88" height="5.48"/>
<rect class="cls-2" x="67.7" y="106.38" width="61.68" height="5.48"/>
<rect class="cls-2" x="67.7" y="117.37" width="42.85" height="5.48"/>
<rect class="cls-2" x="67.7" y="128.36" width="24.02" height="5.48"/>
<rect class="cls-2" x="67.7" y="139.35" width="5.84" height="5.48"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 196.27 196.27">
<rect class="bv-logo__background" width="196.27" height="196.27" />
<g class="bv-logo__wordmark">
<rect x="67.7" y="51.44" width="5.84" height="5.48" />
<rect x="67.7" y="62.43" width="24.02" height="5.48" />
<rect x="67.7" y="73.41" width="42.85" height="5.48" />
<rect x="67.7" y="84.4" width="61.68" height="5.48" />
<rect x="67.7" y="95.39" width="80.88" height="5.48" />
<rect x="67.7" y="106.38" width="61.68" height="5.48" />
<rect x="67.7" y="117.36" width="42.85" height="5.48" />
<rect x="67.7" y="128.35" width="24.02" height="5.48" />
<rect x="67.7" y="139.34" width="5.84" height="5.48" />
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 1007 B

After

Width:  |  Height:  |  Size: 726 B

+13 -25
View File
@@ -1,26 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 196.27 196.27">
<defs>
<style>
.cls-1 {
fill: #fe0032;
}
.cls-2 {
fill: #fff;
}
</style>
</defs>
<rect class="cls-2" width="196.27" height="196.27"/>
<g>
<rect class="cls-1" x="67.7" y="51.44" width="5.84" height="5.48"/>
<rect class="cls-1" x="67.7" y="62.43" width="24.02" height="5.48"/>
<rect class="cls-1" x="67.7" y="73.41" width="42.85" height="5.48"/>
<rect class="cls-1" x="67.7" y="84.4" width="61.68" height="5.48"/>
<rect class="cls-1" x="67.7" y="95.39" width="80.88" height="5.48"/>
<rect class="cls-1" x="67.7" y="106.38" width="61.68" height="5.48"/>
<rect class="cls-1" x="67.7" y="117.37" width="42.85" height="5.48"/>
<rect class="cls-1" x="67.7" y="128.36" width="24.02" height="5.48"/>
<rect class="cls-1" x="67.7" y="139.35" width="5.84" height="5.48"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 196.27 196.27">
<rect class="bv-logo__background" width="196.27" height="196.27" />
<g class="bv-logo__accent">
<rect x="67.7" y="51.44" width="5.84" height="5.48" />
<rect x="67.7" y="62.43" width="24.02" height="5.48" />
<rect x="67.7" y="73.41" width="42.85" height="5.48" />
<rect x="67.7" y="84.4" width="61.68" height="5.48" />
<rect x="67.7" y="95.39" width="80.88" height="5.48" />
<rect x="67.7" y="106.38" width="61.68" height="5.48" />
<rect x="67.7" y="117.36" width="42.85" height="5.48" />
<rect x="67.7" y="128.35" width="24.02" height="5.48" />
<rect x="67.7" y="139.34" width="5.84" height="5.48" />
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 1007 B

After

Width:  |  Height:  |  Size: 724 B

+2 -4
View File
@@ -1,5 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M12,0C18.623,0 24,5.377 24,12C24,18.623 18.623,24 12,24C5.377,24 0,18.623 0,12C0,5.377 5.377,0 12,0M14.557,16L9.442,16C9.988,18.46 10.883,20.114 12,21.744C13.194,20.003 14.041,18.334 14.557,16M7.4,16L2.833,16C4.069,18.825 6.537,20.972 9.588,21.716C8.54,19.983 7.805,18.058 7.4,16M21.167,16L16.6,16C16.209,17.988 15.505,19.887 14.425,21.694C17.437,20.931 19.942,18.799 21.167,16M7.162,10L2.2,10C1.933,11.313 1.933,12.685 2.2,14L7.115,14C6.996,12.671 7.014,11.328 7.162,10M14.823,10L9.176,10C9.011,11.326 8.991,12.672 9.123,14L14.876,14C15.009,12.672 14.987,11.327 14.823,10M21.8,10L16.837,10C16.985,11.328 17.003,12.671 16.885,14L21.8,14C22.06,12.715 22.073,11.352 21.8,10M9.644,2.271C6.567,3.003 4.077,5.157 2.833,8L7.486,8C7.921,5.958 8.664,4.015 9.644,2.271M11.999,2.223C10.91,3.993 10.089,5.676 9.536,8L14.463,8C13.929,5.754 13.126,4.052 11.999,2.223M14.367,2.292C15.38,4.104 16.1,6.052 16.513,8L21.167,8C19.935,5.184 17.405,3.042 14.367,2.292" style="fill:rgb(92,111,138);"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0C18.623 0 24 5.377 24 12c0 6.623-5.377 12-12 12S0 18.623 0 12 5.377 0 12 0Zm2.557 16H9.442c.546 2.46 1.441 4.114 2.558 5.744 1.194-1.741 2.041-3.41 2.557-5.744ZM7.4 16H2.833c1.236 2.825 3.704 4.972 6.755 5.716-.048-1.733-.783-3.658-1.188-5.716Zm13.767 0H16.6c-.391 1.988-1.095 3.887-2.175 5.694 3.012-.763 5.517-2.895 6.742-5.694ZM7.162 10H2.2a7.996 7.996 0 0 0 0 4h4.915a22.266 22.266 0 0 1 .047-4Zm7.661 0H9.176a22.238 22.238 0 0 0-.053 4h5.753a22.238 22.238 0 0 0-.053-4Zm6.638 0h-4.963a22.266 22.266 0 0 1 .048 4H21.8a7.99 7.99 0 0 0-.342-4ZM9.644 2.271A11.996 11.996 0 0 0 2.833 8h4.653c.435-2.042 1.178-3.985 2.158-5.729ZM12 2.223c-1.089 1.77-1.91 3.453-2.463 5.777h4.927C13.93 5.754 13.127 4.052 12 2.223Zm2.368.069C15.38 4.104 16.1 6.052 16.513 8h4.654a11.995 11.995 0 0 0-6.799-5.708Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 901 B

+2 -4
View File
@@ -1,5 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M23.334,11.96C22.621,11.234 22.462,10.131 22.941,9.233C23.283,8.593 23.307,7.832 23.005,7.171C22.704,6.511 22.112,6.029 21.404,5.869C20.413,5.644 19.682,4.802 19.601,3.788C19.542,3.065 19.15,2.41 18.539,2.018C17.93,1.625 17.172,1.54 16.489,1.789C15.533,2.136 14.463,1.821 13.847,1.013C13.407,0.437 12.723,0.098 11.997,0.098C11.272,0.098 10.588,0.437 10.148,1.013C9.535,1.822 8.465,2.137 7.509,1.79C6.827,1.542 6.069,1.627 5.459,2.019C4.849,2.411 4.456,3.066 4.398,3.789C4.316,4.803 3.586,5.646 2.595,5.87C1.887,6.03 1.295,6.512 0.994,7.172C0.693,7.832 0.717,8.594 1.059,9.233C1.538,10.13 1.379,11.234 0.667,11.96C0.158,12.477 -0.08,13.202 0.023,13.92C0.126,14.638 0.559,15.267 1.193,15.62C2.081,16.115 2.545,17.13 2.337,18.125C2.19,18.835 2.381,19.573 2.856,20.121C3.332,20.67 4.036,20.965 4.758,20.919C5.774,20.856 6.711,21.459 7.075,22.408C7.334,23.086 7.895,23.603 8.592,23.807C9.287,24.011 10.039,23.879 10.623,23.45C11.442,22.847 12.559,22.847 13.377,23.45C13.961,23.88 14.713,24.012 15.408,23.807C16.105,23.603 16.666,23.085 16.926,22.408C17.289,21.459 18.227,20.855 19.242,20.919C19.966,20.965 20.669,20.67 21.144,20.121C21.619,19.573 21.811,18.835 21.663,18.125C21.456,17.13 21.919,16.115 22.808,15.62C23.441,15.266 23.873,14.638 23.977,13.92C24.081,13.202 23.842,12.477 23.334,11.96ZM10.75,17.39L6.25,13.026L8.107,11.169L10.75,13.675L16.393,7.891L18.25,9.748L10.75,17.39Z" style="fill:rgb(92,111,138);fill-rule:nonzero;"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" fill-rule="nonzero">
<path d="M23.334 11.96C22.621 11.234 22.462 10.131 22.941 9.233C23.283 8.593 23.307 7.832 23.005 7.171C22.704 6.511 22.112 6.029 21.404 5.869C20.413 5.644 19.682 4.802 19.601 3.788C19.542 3.065 19.15 2.41 18.539 2.018C17.93 1.625 17.172 1.54 16.489 1.789C15.533 2.136 14.463 1.821 13.847 1.013C13.407 0.437 12.723 0.098 11.997 0.098C11.272 0.098 10.588 0.437 10.148 1.013C9.535 1.822 8.465 2.137 7.509 1.79C6.827 1.542 6.069 1.627 5.459 2.019C4.849 2.411 4.456 3.066 4.398 3.789C4.316 4.803 3.586 5.646 2.595 5.87C1.887 6.03 1.295 6.512 0.994 7.172C0.693 7.832 0.717 8.594 1.059 9.233C1.538 10.13 1.379 11.234 0.667 11.96C0.158 12.477 -0.08 13.202 0.023 13.92C0.126 14.638 0.559 15.267 1.193 15.62C2.081 16.115 2.545 17.13 2.337 18.125C2.19 18.835 2.381 19.573 2.856 20.121C3.332 20.67 4.036 20.965 4.758 20.919C5.774 20.856 6.711 21.459 7.075 22.408C7.334 23.086 7.895 23.603 8.592 23.807C9.287 24.011 10.039 23.879 10.623 23.45C11.442 22.847 12.559 22.847 13.377 23.45C13.961 23.88 14.713 24.012 15.408 23.807C16.105 23.603 16.666 23.085 16.926 22.408C17.289 21.459 18.227 20.855 19.242 20.919C19.966 20.965 20.669 20.67 21.144 20.121C21.619 19.573 21.811 18.835 21.663 18.125C21.456 17.13 21.919 16.115 22.808 15.62C23.441 15.266 23.873 14.638 23.977 13.92C24.081 13.202 23.842 12.477 23.334 11.96ZM10.75 17.39L6.25 13.026L8.107 11.169L10.75 13.675L16.393 7.891L18.25 9.748L10.75 17.39Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

+2 -4
View File
@@ -1,5 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M12,6.453L21,14.828L21,24L15,24L15,18L9,18L9,24L3,24L3,14.828L12,6.453ZM24,12.148L12,1L0,12.133L1.361,13.598L12,3.73L22.639,13.613L24,12.148Z" style="fill:rgb(92,111,138);fill-rule:nonzero;"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" fill-rule="nonzero">
<path d="M12 6.453 21 14.828V24h-6v-6H9v6H3v-9.172zM24 12.148 12 1 0 12.133l1.361 1.465L12 3.73l10.639 9.883z"/>
</svg>

Before

Width:  |  Height:  |  Size: 656 B

After

Width:  |  Height:  |  Size: 223 B

+2 -4
View File
@@ -1,5 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M21,7.702L12.5,12.322L12.5,22C14.067,21.135 18.879,18.483 20.477,17.601C20.8,17.424 21,17.082 21,16.71L21,7.702ZM11.5,12.321L3,7.599L3,16.605C3,16.975 3.197,17.313 3.514,17.492C5.104,18.39 9.93,21.115 11.5,22L11.5,12.321ZM3.421,6.692L12,11.455L20.672,6.742C20.672,6.742 14.041,3.004 12.486,2.128C12.335,2.043 12.167,2 12,2C11.832,2 11.665,2.043 11.514,2.128C9.959,3.004 3.421,6.692 3.421,6.692Z" style="fill:rgb(92,111,138);fill-rule:nonzero;"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" fill-rule="nonzero">
<path d="M21 7.702 12.5 12.322V22c1.567-.865 6.379-3.517 7.977-4.399.323-.177.523-.519.523-.891V7.702zm-9.5 4.619L3 7.599v9.006c0 .37.197.708.514.887 1.59.898 6.416 3.623 7.986 4.508v-9.789zm-8.079-5.629L12 11.455l8.672-4.713s-6.631-3.738-8.186-4.614c-.151-.085-.319-.128-.486-.128s-.335.043-.486.128c-1.555.876-8.093 4.564-8.093 4.564Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 909 B

After

Width:  |  Height:  |  Size: 450 B

+49 -95
View File
@@ -1,102 +1,56 @@
<!-- components/application-form.html
<!-- components/application-form.html
https://bitvid.network?modal=application => open nostrFormModal
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Application Form</title>
<!-- Include Tailwind CSS -->
<link href="css/tailwind.generated.css" rel="stylesheet" />
</head>
<body class="bg-gray-900 text-gray-100">
<!-- Application Form Modal -->
<div
id="nostrFormModal"
class="fixed inset-0 z-50 hidden"
style="background: transparent"
>
<!-- Dark/blur overlay -->
<div
class="absolute inset-0 z-10"
style="
background-color: rgba(0, 0, 0, 0.9);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
"
></div>
<div
id="nostrFormModal"
class="bv-modal hidden items-start justify-center"
role="dialog"
aria-modal="true"
aria-labelledby="nostrFormModalTitle"
aria-describedby="nostrFormModalDescription"
data-ds="new"
>
<div class="bv-modal-backdrop" data-dismiss></div>
<!-- Outer container with full width and centered content -->
<div
class="modal-container relative h-full w-full flex items-start justify-center overflow-y-auto z-20"
<div class="bv-modal__panel modal-sheet max-h-modal-shell overflow-y-auto" tabindex="-1">
<!-- Header bar with updated styling -->
<div class="modal-header">
<h2
id="nostrFormModalTitle"
class="pb-2 text-3xl font-extrabold tracking-wide text-text"
>
<!-- Inner content container -->
<div
class="modal-content bg-gray-900 w-full max-w-[90%] lg:max-w-6xl my-0 rounded-lg overflow-hidden relative"
Application Form
</h2>
<button
id="closeNostrFormModal"
class="modal-close"
aria-label="Close application form modal"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<!-- Header bar with updated styling -->
<div
class="sticky top-0 bg-gradient-to-b from-black/80 to-transparent p-4 flex items-center justify-between border-b border-gray-700"
>
<h2 class="text-3xl font-extrabold text-white tracking-wide pb-2">
Application Form
</h2>
<button
id="closeNostrFormModal"
class="flex items-center justify-center w-10 h-10 rounded-full bg-black/50 hover:bg-black/70 transition-all duration-200 backdrop-blur focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-black focus:ring-blue-500"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-gray-300"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<!-- Main Content -->
<div class="p-6">
<div class="w-full" style="height: 80vh">
<iframe
src="./components/iframe_forms/iframe-application-form.html"
class="w-full h-full"
frameborder="0"
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"
></iframe>
</div>
</div>
</div>
</div>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<!-- Optional Script for Opening/Closing the Modal -->
<script>
const openBtn = document.getElementById("openNostrForm");
const appModal = document.getElementById("nostrFormModal");
const closeBtn = document.getElementById("closeNostrFormModal");
// Open the modal when the designated button is clicked.
if (openBtn) {
openBtn.addEventListener("click", () => {
appModal.classList.remove("hidden");
});
}
// Close the modal when the close button is clicked.
if (closeBtn) {
closeBtn.addEventListener("click", () => {
appModal.classList.add("hidden");
});
}
</script>
</body>
</html>
<div id="nostrFormModalDescription" class="modal-body">
<div class="modal-embed-frame">
<iframe
src="./components/iframe_forms/iframe-application-form.html"
class="h-full w-full rounded-lg border border-border/30 shadow-sm"
frameborder="0"
></iframe>
</div>
</div>
</div>
</div>
+44 -54
View File
@@ -1,67 +1,57 @@
<!-- components/bug-fix-form.html
<!-- components/bug-fix-form.html
// https://bitvid.network?modal=bug => open bugFixModal
// Admin: npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe
// Form:
// Responses:
// Form:
// Responses:
-->
<div
id="bugFixModal"
class="fixed inset-0 z-50 hidden"
style="background: transparent"
class="bv-modal hidden items-start justify-center"
role="dialog"
aria-modal="true"
aria-labelledby="bugFixModalTitle"
aria-describedby="bugFixModalDescription"
data-ds="new"
>
<!-- Dark/blur overlay -->
<div
class="absolute inset-0 z-10"
style="
background-color: rgba(0, 0, 0, 0.9);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
"
></div>
<div class="bv-modal-backdrop" data-dismiss></div>
<div
class="modal-container relative h-full w-full flex items-start justify-center overflow-y-auto z-20"
>
<div
class="modal-content bg-gray-900 w-full max-w-[90%] lg:max-w-6xl my-0 rounded-lg overflow-hidden relative"
>
<!-- Header bar (sticky) with updated title styling -->
<div
class="sticky top-0 bg-gradient-to-b from-black/80 to-transparent p-4 flex items-center justify-between border-b border-gray-700"
<div class="bv-modal__panel modal-sheet max-h-modal-shell overflow-y-auto" tabindex="-1">
<!-- Header bar (sticky) with updated title styling -->
<div class="modal-header">
<h2
id="bugFixModalTitle"
class="pb-2 text-3xl font-extrabold tracking-wide text-text"
>
<h2 class="text-3xl font-extrabold text-white tracking-wide pb-2">
Bug Fix Form
</h2>
<button
id="closeBugFixModal"
class="flex items-center justify-center w-10 h-10 rounded-full bg-black/50 hover:bg-black/70 transition-all duration-200 backdrop-blur focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-black focus:ring-blue-500"
Bug Fix Form
</h2>
<button
id="closeBugFixModal"
class="modal-close"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-gray-300"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<div class="p-6">
<div class="w-full" style="height: 80vh">
<iframe
src="./components/iframe_forms/iframe-bug-fix-form.html"
class="w-full h-full"
frameborder="0"
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"
></iframe>
</div>
<div id="bugFixModalDescription" class="modal-body">
<div class="modal-embed-frame">
<iframe
src="./components/iframe_forms/iframe-bug-fix-form.html"
class="h-full w-full rounded-lg border border-border/30 shadow-sm"
frameborder="0"
></iframe>
</div>
</div>
</div>
+42 -52
View File
@@ -1,64 +1,54 @@
<!-- components/content-appeals-form.html
<!-- components/content-appeals-form.html
https://bitvid.network?modal=appeals => open contentAppealsModal
-->
<div
id="contentAppealsModal"
class="fixed inset-0 z-50 hidden"
style="background: transparent"
class="bv-modal hidden items-start justify-center"
role="dialog"
aria-modal="true"
aria-labelledby="contentAppealsModalTitle"
aria-describedby="contentAppealsModalDescription"
data-ds="new"
>
<!-- Dark/blur overlay -->
<div
class="absolute inset-0 z-10"
style="
background-color: rgba(0, 0, 0, 0.9);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
"
></div>
<div class="bv-modal-backdrop" data-dismiss></div>
<div
class="modal-container relative h-full w-full flex items-start justify-center overflow-y-auto z-20"
>
<div
class="modal-content bg-gray-900 w-full max-w-[90%] lg:max-w-6xl my-0 rounded-lg overflow-hidden relative"
>
<!-- Header bar (sticky) with improved title styling -->
<div
class="sticky top-0 bg-gradient-to-b from-black/80 to-transparent p-4 flex items-center justify-between border-b border-gray-700"
<div class="bv-modal__panel modal-sheet max-h-modal-shell overflow-y-auto" tabindex="-1">
<!-- Header bar (sticky) with improved title styling -->
<div class="modal-header">
<h2
id="contentAppealsModalTitle"
class="pb-2 text-3xl font-extrabold tracking-wide text-text"
>
<h2 class="text-3xl font-extrabold text-white tracking-wide pb-2">
Content Appeals Form
</h2>
<button
id="closeContentAppealsModal"
class="flex items-center justify-center w-10 h-10 rounded-full bg-black/50 hover:bg-black/70 transition-all duration-200 backdrop-blur focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-black focus:ring-blue-500"
Content Appeals Form
</h2>
<button
id="closeContentAppealsModal"
class="modal-close"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-gray-300"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<div class="p-6">
<div class="w-full" style="height: 80vh">
<iframe
src="./components/iframe_forms/iframe-content-appeals-form.html"
class="w-full h-full"
frameborder="0"
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"
></iframe>
</div>
<div id="contentAppealsModalDescription" class="modal-body">
<div class="modal-embed-frame">
<iframe
src="./components/iframe_forms/iframe-content-appeals-form.html"
class="h-full w-full rounded-lg border border-border/30 shadow-sm"
frameborder="0"
></iframe>
</div>
</div>
</div>
+71
View File
@@ -0,0 +1,71 @@
<!-- components/delete-video-modal.html -->
<div
id="deleteVideoModal"
class="bv-modal hidden items-start justify-center md:items-center"
role="dialog"
aria-modal="true"
aria-labelledby="deleteModalTitle"
aria-describedby="deleteModalDescription"
data-ds="new"
>
<div
id="deleteVideoModalOverlay"
class="bv-modal-backdrop"
data-dismiss
></div>
<div class="modal-sheet max-h-modal-shell w-full max-w-lg overflow-y-auto" tabindex="-1">
<div class="modal-header">
<div class="flex flex-col">
<h2 id="deleteModalTitle" class="text-xl font-bold text-text">Delete video</h2>
<p id="deleteModalSubtitle" class="text-xs text-muted">
This action removes the note from every feed.
</p>
</div>
<button
id="closeDeleteVideoModal"
class="modal-close"
type="button"
aria-label="Close delete modal"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="modal-body space-y-6">
<div class="rounded-md border border-critical/40 bg-critical/10 p-4 text-sm text-critical">
<p class="font-semibold">Deleting publishes tombstones for every revision.</p>
<p class="mt-2 text-xs">
Relays and clients will treat the note as deleted. This cannot be undone.
</p>
</div>
<div class="space-y-4 text-sm text-text">
<p id="deleteModalDescription">
Are you sure you want to delete this video? Viewers will no longer see it in any grid.
</p>
<dl id="deleteModalMetadata" class="grid gap-3 text-xs text-muted sm:grid-cols-2"></dl>
</div>
</div>
<div class="modal-footer flex flex-col gap-3 sm:flex-row sm:justify-end">
<button type="button" id="cancelDeleteVideo" class="btn-ghost w-full sm:w-auto">Cancel</button>
<button
type="button"
id="confirmDeleteVideo"
class="btn w-full sm:w-auto"
data-variant="critical"
>
Delete all versions
</button>
</div>
</div>
</div>
+118 -107
View File
@@ -1,127 +1,138 @@
<!-- Disclaimer Modal (wide, matching other modals) -->
<div
id="disclaimerModal"
class="fixed inset-0 z-50 hidden"
style="background: transparent"
class="bv-modal hidden items-start justify-center"
role="dialog"
aria-modal="true"
aria-labelledby="disclaimerModalTitle"
aria-describedby="disclaimerModalDescription"
data-ds="new"
>
<!-- Dark/blur overlay -->
<div
class="absolute inset-0 z-10"
style="
background-color: rgba(0, 0, 0, 0.9);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
"
></div>
<div class="bv-modal-backdrop" data-dismiss></div>
<!-- Outer container with wide layout, just like content-appeals-form -->
<div
class="modal-container relative h-full w-full flex items-start justify-center overflow-y-auto z-20"
>
<!-- The .modal-content, same classes: bg-gray-900, w-full max-w-[90%], etc. -->
<div
class="modal-content bg-gray-900 w-full max-w-[90%] lg:max-w-6xl my-0 rounded-lg overflow-hidden relative flex flex-col"
>
<!-- Sticky top bar, if you want a top heading or 'X' button up here -->
<div
class="modal-header sticky top-0 bg-gradient-to-b from-black/80 to-transparent p-4 flex items-center justify-between"
>
</div>
<!-- Main Content -->
<div class="modal-body flex-1 overflow-y-auto">
<!-- Scrollable disclaimers area -->
<div class="space-y-6 text-gray-300">
<!-- Example: Insert your disclaimers here -->
<div class="flex justify-center mb-8">
<img
src="assets/svg/bitvid-logo-dark-mode.svg"
alt="BitVid Logo"
class="h-16"
/>
</div>
<h2 class="text-2xl font-bold text-white mb-0">Welcome to bitvid</h2>
<!-- Warning Alert -->
<div
class="bg-yellow-900/20 border border-yellow-700/50 rounded-lg p-4 mb-6 flex items-start"
<div class="bv-modal__panel modal-sheet max-h-modal-shell flex flex-col overflow-hidden" tabindex="-1">
<!-- Main Content -->
<div class="modal-body flex-1 overflow-y-auto">
<div class="mx-auto max-w-3xl space-y-8 text-base text-text">
<div class="flex justify-center pt-1">
<span
class="inline-flex h-2xl items-center text-text bv-logo"
role="img"
aria-label="bitvid logo"
>
<svg
class="h-5 w-5 text-yellow-500 mt-0.5 mr-3 flex-shrink-0"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
class="h-full w-auto"
viewBox="0 0 318.32 108.98"
aria-hidden="true"
focusable="false"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
<g class="bv-logo__accent">
<rect x="7.1" y="7.8" width="5.84" height="5.48" />
<rect x="7.1" y="18.78" width="24.02" height="5.48" />
<rect x="7.1" y="29.77" width="42.85" height="5.48" />
<rect x="7.1" y="40.76" width="61.68" height="5.48" />
<rect x="7.1" y="51.75" width="80.88" height="5.48" />
<rect x="7.1" y="62.74" width="61.68" height="5.48" />
<rect x="7.1" y="73.73" width="42.85" height="5.48" />
<rect x="7.1" y="84.72" width="24.02" height="5.48" />
<rect x="7.1" y="95.71" width="5.84" height="5.48" />
</g>
<g class="bv-logo__wordmark">
<path d="M121.28 25.08v44.54c0 5.67 2.79 8.54 8.22 8.54h11.57c5.51 0 8.3-2.87 8.3-8.54v-13.09c0-5.67-2.79-8.46-8.3-8.46h-16.6v-5.83h17.72c8.7 0 13.01 4.47 13.01 13.33v15.01c0 8.86-4.31 13.33-13.01 13.33h-13.73c-8.7 0-13.01-4.47-13.01-13.33V25.08h5.83Z" />
<path d="M168.53 25.08v6.26h-5.83v-6.26h5.83Zm0 17.56v41.27h-5.83v-41.27h5.83Z" />
<path d="M191.36 25.08v17.16h9.02v5.19h-9.02v36.48h-5.83v-36.48h-9.82v-5.19h9.82V25.08h5.83Z" />
<path d="M212.88 42.24l16.84 35.68 16.6-35.68h6.07l-19.56 41.67h-6.31l-20.04-41.67h6.39Z" />
<path d="M264.05 25.08v6.26h-5.83v-6.26h5.83Zm0 17.16v41.67h-5.83v-41.67h5.83Z" />
<path d="M311.21 25.08v45.5c0 8.86-4.31 13.33-13.01 13.33h-13.73c-8.7 0-13.01-4.47-13.01-13.33v-15.01c0-8.86 4.31-13.33 13.01-13.33h17.72v5.83h-16.6c-5.51 0-8.3 2.79-8.3 8.46v13.09c0 5.67 2.79 8.54 8.3 8.54h11.49c5.51 0 8.3-2.87 8.3-8.54V25.08h5.83Z" />
</g>
</svg>
<p class="text-yellow-200">
This platform is currently in development and only supports Chrome
and Firefox-based browsers. Other browsers are not supported at
this time. You may encounter bugs or missing features. Give it a
sec. Videos might take 10 to 60 seconds to load initially.
</p>
</div>
<p>
</span>
</div>
<header class="space-y-4 text-center">
<h2
id="disclaimerModalTitle"
class="text-4xl font-semibold tracking-tight text-text"
>
Welcome to bitvid
</h2>
<p class="mx-auto max-w-2xl text-base leading-relaxed text-text-frosted">
bitvid is a decentralized video platform where content is shared
directly between users. We want you to understand a few important
points before you continue:
points before you continue.
</p>
</header>
<div class="modal-alert" role="status">
<svg
class="modal-alert__icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
<p class="text-sm">
This platform is currently in development and only supports Chrome
and Firefox-based browsers. Other browsers are not supported at
this time. You may encounter bugs or missing features. Give it a
sec—videos might take 10 to 60 seconds to load initially.
</p>
</div>
<div class="space-y-4">
<div class="bg-gray-800 rounded-lg p-4">
<h3 class="text-white font-semibold mb-2">Early Access Status</h3>
<p class="text-gray-400">
Currently, video posting is invite-only as we carefully scale
our platform. While anyone can watch videos, content creation is
limited to approved creators. This helps us maintain quality
content during our early stages.
</p>
</div>
<div class="bg-gray-800 rounded-lg p-4">
<h3 class="text-white font-semibold mb-2">
Content Responsibility & Moderation
</h3>
<p class="text-gray-400">
While we don't host videos directly, we maintain community
standards through access control. Users who violate our
guidelines may be blocked from accessing the platform. All
content must follow local laws and platform guidelines.
</p>
</div>
<div class="bg-gray-800 rounded-lg p-4">
<h3 class="text-white font-semibold mb-2">Platform Status</h3>
<p class="text-gray-400">
bitvid is a work in progress. Features may change or break, and
security improvements are ongoing. Your feedback and patience
help us build a better platform.
</p>
</div>
<div class="bg-gray-800 rounded-lg p-4">
<h3 class="text-white font-semibold mb-2">Get Involved</h3>
<p class="text-gray-400">
Are you a developer? We'd love your help! Visit our GitHub
repository to contribute to building the future of decentralized
video sharing.
</p>
</div>
</div>
<div id="disclaimerModalDescription" class="space-y-4">
<article class="modal-tile space-y-2">
<h3>Early Access Status</h3>
<p>
Currently, video posting is invite-only as we carefully scale
our platform. While anyone can watch videos, content creation is
limited to approved creators. This helps us maintain quality
content during our early stages.
</p>
</article>
<article class="modal-tile space-y-2">
<h3>Content Responsibility &amp; Moderation</h3>
<p>
While we don't host videos directly, we maintain community
standards through access control. Users who violate our
guidelines may be blocked from accessing the platform. All
content must follow local laws and platform guidelines.
</p>
</article>
<article class="modal-tile space-y-2">
<h3>Platform Status</h3>
<p>
bitvid is a work in progress. Features may change or break, and
security improvements are ongoing. Your feedback and patience
help us build a better platform.
</p>
</article>
<article class="modal-tile space-y-2">
<h3>Get Involved</h3>
<p>
Are you a developer? We'd love your help! Visit our GitHub
repository to contribute to building the future of decentralized
video sharing.
</p>
</article>
</div>
</div>
</div>
<!-- Button at bottom -->
<div class="modal-footer">
<button
id="acceptDisclaimer"
class="w-full bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-black transition-colors duration-200"
>
I Understand
</button>
</div>
<!-- Button at bottom -->
<div class="modal-footer pt-6 pb-16 md:pb-12">
<button
id="acceptDisclaimer"
class="modal-primary"
type="button"
>
I Understand
</button>
</div>
</div>
</div>
File diff suppressed because it is too large. Load diff
+43 -53
View File
@@ -1,64 +1,54 @@
<!-- components/feature-request-form.html
// https://bitvid.network?modal=feature => open featureRequestModal
<!-- components/feature-request-form.html
// https://bitvid.network?modal=feature => open featureRequestModal
-->
<div
id="featureRequestModal"
class="fixed inset-0 z-50 hidden"
style="background: transparent"
class="bv-modal hidden items-start justify-center"
role="dialog"
aria-modal="true"
aria-labelledby="featureRequestModalTitle"
aria-describedby="featureRequestModalDescription"
data-ds="new"
>
<!-- Dark/blur overlay -->
<div
class="absolute inset-0 z-10"
style="
background-color: rgba(0, 0, 0, 0.9);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
"
></div>
<div class="bv-modal-backdrop" data-dismiss></div>
<div
class="modal-container relative h-full w-full flex items-start justify-center overflow-y-auto z-20"
>
<div
class="modal-content bg-gray-900 w-full max-w-[90%] lg:max-w-6xl my-0 rounded-lg overflow-hidden relative"
>
<!-- Header bar (sticky) with updated title styling -->
<div
class="sticky top-0 bg-gradient-to-b from-black/80 to-transparent p-4 flex items-center justify-between border-b border-gray-700"
<div class="bv-modal__panel modal-sheet max-h-modal-shell overflow-y-auto" tabindex="-1">
<!-- Header bar (sticky) with updated title styling -->
<div class="modal-header">
<h2
id="featureRequestModalTitle"
class="pb-2 text-3xl font-extrabold tracking-wide text-text"
>
<h2 class="text-3xl font-extrabold text-white tracking-wide pb-2">
Feature Request Form
</h2>
<button
id="closeFeatureRequestModal"
class="flex items-center justify-center w-10 h-10 rounded-full bg-black/50 hover:bg-black/70 transition-all duration-200 backdrop-blur focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-black focus:ring-blue-500"
Feature Request Form
</h2>
<button
id="closeFeatureRequestModal"
class="modal-close"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-gray-300"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<div class="p-6">
<div class="w-full" style="height: 80vh">
<iframe
src="./components/iframe_forms/iframe-request-form.html"
class="w-full h-full"
frameborder="0"
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"
></iframe>
</div>
<div id="featureRequestModalDescription" class="modal-body">
<div class="modal-embed-frame">
<iframe
src="./components/iframe_forms/iframe-request-form.html"
class="h-full w-full rounded-lg border border-border/30 shadow-sm"
frameborder="0"
></iframe>
</div>
</div>
</div>
+45 -55
View File
@@ -1,68 +1,58 @@
<!-- components/content-appeals-form.html
<!-- components/content-appeals-form.html
// https://bitvid.network?modal=feedback => open generalFeedbackModal
//
//
// Admin: npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe
// Form:
// Responses:
// Form:
// Responses:
-->
<div
id="generalFeedbackModal"
class="fixed inset-0 z-50 hidden"
style="background: transparent"
class="bv-modal hidden items-start justify-center"
role="dialog"
aria-modal="true"
aria-labelledby="generalFeedbackModalTitle"
aria-describedby="generalFeedbackModalDescription"
data-ds="new"
>
<!-- Dark/blur overlay -->
<div
class="absolute inset-0 z-10"
style="
background-color: rgba(0, 0, 0, 0.9);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
"
></div>
<div class="bv-modal-backdrop" data-dismiss></div>
<div
class="modal-container relative h-full w-full flex items-start justify-center overflow-y-auto z-20"
>
<div
class="modal-content bg-gray-900 w-full max-w-[90%] lg:max-w-6xl my-0 rounded-lg overflow-hidden relative"
>
<!-- Header bar (sticky) with updated title styling -->
<div
class="sticky top-0 bg-gradient-to-b from-black/80 to-transparent p-4 flex items-center justify-between border-b border-gray-700"
<div class="bv-modal__panel modal-sheet max-h-modal-shell overflow-y-auto" tabindex="-1">
<!-- Header bar (sticky) with updated title styling -->
<div class="modal-header">
<h2
id="generalFeedbackModalTitle"
class="pb-2 text-3xl font-extrabold tracking-wide text-text"
>
<h2 class="text-3xl font-extrabold text-white tracking-wide pb-2">
General Feedback Form
</h2>
<button
id="closeGeneralFeedbackModal"
class="flex items-center justify-center w-10 h-10 rounded-full bg-black/50 hover:bg-black/70 transition-all duration-200 backdrop-blur focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-black focus:ring-blue-500"
General Feedback Form
</h2>
<button
id="closeGeneralFeedbackModal"
class="modal-close"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-gray-300"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<div class="p-6">
<div class="w-full" style="height: 80vh">
<iframe
src="./components/iframe_forms/iframe-feedback-form.html"
class="w-full h-full"
frameborder="0"
style="border: none; box-shadow: 0 0 2px rgba(0, 0, 0, 0.2)"
></iframe>
</div>
<div id="generalFeedbackModalDescription" class="modal-body">
<div class="modal-embed-frame">
<iframe
src="./components/iframe_forms/iframe-feedback-form.html"
class="h-full w-full rounded-lg border border-border/30 shadow-sm"
frameborder="0"
></iframe>
</div>
</div>
</div>
@@ -3,136 +3,14 @@
<head>
<meta charset="utf-8" />
<title>bitvid Whitelist Application Form</title>
<script type="module" src="./theme-sync.js"></script>
<!-- Link to your main stylesheet -->
<link rel="stylesheet" href="../../css/style.css?v=2024.10.29" />
<style>
/* Override for form page to match modal field styling */
/* Remove width constraints from body so our container can be full width */
body {
background-color: transparent;
color: #fff;
font-family: system-ui, -apple-system, sans-serif;
margin: 20px;
}
/* Override the .container to use the full available width */
.container {
width: 100%;
max-width: 100%;
margin: 0;
padding: 0;
}
/* Card-like container for the form */
.form-container {
background-color: #111827; /* Tailwind's bg-gray-900 */
padding: 1.5rem;
border-radius: 0.5rem;
box-shadow: var(--shadow-md);
}
h1,
h2 {
color: #fff;
}
/* Labels in a light gray */
label {
display: block;
margin-top: 1em;
font-weight: bold;
color: #e5e7eb; /* Tailwind's text-gray-200 */
}
/* Input, textarea, select mimic modal field styles */
input,
textarea,
select {
width: 100%;
margin-bottom: 0.75em;
background-color: #1f2937; /* Tailwind's bg-gray-800 */
color: #f3f4f6; /* Tailwind's text-gray-100 */
border: 1px solid #374151; /* Tailwind's border-gray-700 */
padding: 0.5em;
border-radius: 0.375rem; /* rounded-md */
box-sizing: border-box;
}
input:focus,
textarea:focus,
select:focus {
border-color: #3b82f6; /* blue-500 */
outline: none;
box-shadow: 0 0 0 1px #3b82f6;
}
/* Style for checkboxes – display inline with a label */
.checkbox-group label {
display: inline-block;
margin-right: 1rem;
font-weight: normal;
}
/* Button styled similarly to modal publish button */
button {
padding: 0.5em 1em;
background: #3b82f6; /* blue-500 */
color: #fff;
border: none;
border-radius: 0.375rem;
cursor: pointer;
}
/* Status log area */
#status {
margin-top: 1em;
padding: 0.5em;
background: #111827;
white-space: pre-wrap;
min-height: 80px;
border-radius: 0.25rem;
}
.status-line {
margin: 0.25em 0;
}
.error {
color: #f87171; /* a red tint */
}
.success {
color: #3b82f6; /* blue-500 */
}
.warn {
color: #facc15; /* a yellow tone */
}
/* Custom Scrollbar styling for WebKit browsers */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
}
::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.3);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(255, 255, 255, 0.4);
}
/* Custom Scrollbar styling for Firefox */
* {
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
}
</style>
<link rel="stylesheet" href="../../css/tailwind.generated.css" />
<!-- Load nostr‑tools v2.10.4 -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/nostr.bundle.min.js"></script>
</head>
<body>
<body class="iframe-form" data-ds="new">
<div class="container">
<div class="form-container">
<p>
@@ -146,6 +24,7 @@
<h2>1. Applicant Information</h2>
<label for="applicantNpub">Nostr Public Key (npub):</label>
<input
class="form-control"
type="text"
id="applicantNpub"
placeholder="Enter your npub"
@@ -156,6 +35,7 @@
>Preferred Contact Method (if applicable):</label
>
<input
class="form-control"
type="text"
id="contactMethod"
placeholder="Nostr DM, email, or other"
@@ -163,6 +43,7 @@
<label for="username">Username or Alias (if applicable):</label>
<input
class="form-control"
type="text"
id="username"
placeholder="Enter your preferred name"
@@ -175,11 +56,17 @@
</p>
<div class="checkbox-group">
<label
><input type="checkbox" name="contentType" value="Educational" />
><input
class="checkbox"
type="checkbox"
name="contentType"
value="Educational"
/>
Educational</label
>
<label
><input
class="checkbox"
type="checkbox"
name="contentType"
value="Entertainment"
@@ -188,6 +75,7 @@
>
<label
><input
class="checkbox"
type="checkbox"
name="contentType"
value="News & Journalism"
@@ -196,6 +84,7 @@
>
<label
><input
class="checkbox"
type="checkbox"
name="contentType"
value="Creative Works"
@@ -204,6 +93,7 @@
>
<label
><input
class="checkbox"
type="checkbox"
name="contentType"
value="Discussions & Opinions"
@@ -211,12 +101,18 @@
Discussions & Opinions</label
>
<label
><input type="checkbox" name="contentType" value="Other" />
><input
class="checkbox"
type="checkbox"
name="contentType"
value="Other"
/>
Other</label
>
</div>
<label for="otherContent">Other (please specify):</label>
<input
class="form-control"
type="text"
id="otherContent"
placeholder="Describe if Other was selected"
@@ -224,6 +120,7 @@
<label for="whyJoin">Why do you want to join bitvid?</label>
<textarea
class="textarea"
id="whyJoin"
rows="3"
placeholder="Explain your motivation"
@@ -232,7 +129,7 @@
<label for="priorExperience"
>Have you created content on other platforms before?</label
>
<select id="priorExperience">
<select class="select" id="priorExperience">
<option value="">Select an option</option>
<option value="Yes">Yes</option>
<option value="No">No</option>
@@ -242,6 +139,7 @@
>If yes, provide links or references to your previous work:</label
>
<textarea
class="textarea"
id="experienceLinks"
rows="2"
placeholder="Paste links or describe your work"
@@ -252,7 +150,7 @@
<label for="familiarGuidelines"
>Are you familiar with bitvid’s Community Guidelines?</label
>
<select id="familiarGuidelines">
<select class="select" id="familiarGuidelines">
<option value="">Select an option</option>
<option value="Yes">Yes</option>
<option value="No">No</option>
@@ -262,7 +160,7 @@
>Do you agree to follow the guidelines and respect decentralized
moderation?</label
>
<select id="agreeGuidelines">
<select class="select" id="agreeGuidelines">
<option value="">Select an option</option>
<option value="Yes">Yes</option>
<option value="No">No</option>
@@ -273,6 +171,7 @@
community?</label
>
<textarea
class="textarea"
id="communityContribution"
rows="3"
placeholder="Explain your approach"
@@ -285,6 +184,7 @@
bitvid?</label
>
<textarea
class="textarea"
id="specialSkills"
rows="3"
placeholder="e.g., software development, moderation, design, advocacy"
@@ -293,7 +193,7 @@
<label for="testFeatures"
>Would you be interested in helping test new features?</label
>
<select id="testFeatures">
<select class="select" id="testFeatures">
<option value="">Select an option</option>
<option value="Yes">Yes</option>
<option value="No">No</option>
@@ -309,17 +209,32 @@
revoked if guidelines are violated.
</p>
<label for="signature">Signature (Digital or Written):</label>
<input type="text" id="signature" placeholder="Your signature" />
<input
class="form-control"
type="text"
id="signature"
placeholder="Your signature"
/>
<label for="declarationDate">Date:</label>
<input type="date" id="declarationDate" />
<input class="form-control" type="date" id="declarationDate" />
<button type="submit">Submit Application</button>
<button class="btn" type="submit">Submit Application</button>
</form>
<div id="status"></div>
</div>
</div>
<script>
// Provide no-op fallback loggers so embeds stay functional if bitvidLogger is unavailable.
const noopLogger = {
log: () => {},
info: () => {},
warn: () => {},
error: () => {},
};
const dev = window.bitvidLogger?.dev || noopLogger;
const user = window.bitvidLogger?.user || noopLogger;
document.addEventListener("DOMContentLoaded", () => {
// Logging functions for both on-page and console output.
function log(msg, type = "info") {
@@ -330,7 +245,18 @@
if (type === "warn") div.classList.add("warn");
div.textContent = msg;
document.getElementById("status").appendChild(div);
console.log(`[${type.toUpperCase()}] ${msg}`);
const formatted = `[${type.toUpperCase()}] ${msg}`;
if (type === "error") {
user.error(formatted);
} else if (type === "warn") {
user.warn(formatted);
} else if (type === "success") {
user.log(formatted);
} else if (type === "info") {
dev.info(formatted);
} else {
dev.log(formatted);
}
}
function clear() {
document.getElementById("status").innerHTML = "";
@@ -553,10 +479,11 @@ For further questions, contact us through bitvid’s Nostr support channels.
);
} catch (err) {
log("Error: " + err.message, "error");
console.error(err);
user.error(err);
}
});
});
</script>
<div id="uiOverlay" data-overlay-root aria-hidden="true"></div>
</body>
</html>
+73 -137
View File
@@ -3,136 +3,13 @@
<head>
<meta charset="utf-8" />
<title>bitvid Bug Report Form</title>
<script type="module" src="./theme-sync.js"></script>
<!-- Link to your main stylesheet -->
<link rel="stylesheet" href="../../css/style.css?v=2024.10.29" />
<style>
/* Override for form page to match modal field styling */
/* Remove width constraints from body so our container can be full width */
body {
background-color: transparent;
color: #fff;
font-family: system-ui, -apple-system, sans-serif;
margin: 20px;
}
/* Override the .container to use the full available width */
.container {
width: 100%;
max-width: 100%;
margin: 0;
padding: 0;
}
/* Card-like container for the form, similar to modal-content */
.form-container {
background-color: #111827; /* Tailwind's bg-gray-900 */
padding: 1.5rem;
border-radius: 0.5rem;
box-shadow: var(--shadow-md);
}
h1,
h2 {
color: #fff;
}
/* Labels in a light gray */
label {
display: block;
margin-top: 1em;
font-weight: bold;
color: #e5e7eb; /* Tailwind's text-gray-200 */
}
/* Input, textarea, and select mimic modal field styles */
input,
textarea,
select {
width: 100%;
margin-bottom: 0.75em;
background-color: #1f2937; /* Tailwind's bg-gray-800 */
color: #f3f4f6; /* Tailwind's text-gray-100 */
border: 1px solid #374151; /* Tailwind's border-gray-700 */
padding: 0.5em;
border-radius: 0.375rem; /* rounded-md */
box-sizing: border-box;
}
input:focus,
textarea:focus,
select:focus {
border-color: #3b82f6; /* blue-500 */
outline: none;
box-shadow: 0 0 0 1px #3b82f6;
}
/* Checkbox group styling */
.checkbox-group label {
display: inline-block;
margin-right: 1rem;
font-weight: normal;
}
/* Button styled similarly to modal publish button */
button {
padding: 0.5em 1em;
background: #3b82f6; /* blue-500 */
color: #fff;
border: none;
border-radius: 0.375rem;
cursor: pointer;
}
/* Status log area */
#status {
margin-top: 1em;
padding: 0.5em;
background: #111827;
white-space: pre-wrap;
min-height: 80px;
border-radius: 0.25rem;
}
.status-line {
margin: 0.25em 0;
}
.error {
color: #f87171; /* a red tint */
}
.success {
color: #3b82f6; /* blue-500 */
}
.warn {
color: #facc15; /* a yellow tone */
}
/* Custom Scrollbar styling for WebKit browsers */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
}
::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.3);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(255, 255, 255, 0.4);
}
/* Custom Scrollbar styling for Firefox */
* {
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
}
</style>
<link rel="stylesheet" href="../../css/tailwind.generated.css" />
<!-- Load nostr‑tools v2.10.4 -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/nostr.bundle.min.js"></script>
</head>
<body>
<body class="iframe-form" data-ds="new">
<div class="container">
<div class="form-container">
<p>
@@ -146,6 +23,7 @@
<h2>1. User Information</h2>
<label for="userNpub">Nostr Public Key (npub) (optional):</label>
<input
class="form-control"
type="text"
id="userNpub"
placeholder="Enter your npub (optional)"
@@ -153,15 +31,26 @@
<p>Are you a (check all that apply):</p>
<div class="checkbox-group">
<label
><input type="checkbox" name="userRole" value="Viewer" />
><input
class="checkbox"
type="checkbox"
name="userRole"
value="Viewer"
/>
Viewer</label
>
<label
><input type="checkbox" name="userRole" value="Content Creator" />
><input
class="checkbox"
type="checkbox"
name="userRole"
value="Content Creator"
/>
Content Creator</label
>
<label
><input
class="checkbox"
type="checkbox"
name="userRole"
value="Developer/Contributor"
@@ -174,24 +63,28 @@
<h2>2. Bug Details</h2>
<label for="issueDescription">Describe the issue:</label>
<textarea
class="textarea"
id="issueDescription"
rows="3"
placeholder="Provide a clear and concise description of the problem"
></textarea>
<label for="stepsToReproduce">Steps to reproduce the bug:</label>
<textarea
class="textarea"
id="stepsToReproduce"
rows="3"
placeholder="List the actions taken before encountering the issue"
></textarea>
<label for="expectedBehavior">Expected behavior:</label>
<textarea
class="textarea"
id="expectedBehavior"
rows="2"
placeholder="What should have happened instead?"
></textarea>
<label for="actualBehavior">Actual behavior:</label>
<textarea
class="textarea"
id="actualBehavior"
rows="2"
placeholder="What actually happened?"
@@ -202,26 +95,43 @@
<p>What device were you using? (check all that apply):</p>
<div class="checkbox-group">
<label
><input type="checkbox" name="deviceUsed" value="Desktop" />
><input
class="checkbox"
type="checkbox"
name="deviceUsed"
value="Desktop"
/>
Desktop</label
>
<label
><input type="checkbox" name="deviceUsed" value="Mobile" />
><input
class="checkbox"
type="checkbox"
name="deviceUsed"
value="Mobile"
/>
Mobile</label
>
<label
><input type="checkbox" name="deviceUsed" value="Tablet" />
><input
class="checkbox"
type="checkbox"
name="deviceUsed"
value="Tablet"
/>
Tablet</label
>
</div>
<label for="operatingSystem">Operating System:</label>
<input
class="form-control"
type="text"
id="operatingSystem"
placeholder="e.g., Windows, macOS, Linux, iOS, Android"
/>
<label for="browserInfo">Browser (if applicable):</label>
<input
class="form-control"
type="text"
id="browserInfo"
placeholder="e.g., Chrome, Firefox, Safari"
@@ -229,7 +139,7 @@
<label for="usingVPN"
>Are you using a VPN or privacy-focused browser settings?</label
>
<select id="usingVPN">
<select class="select" id="usingVPN">
<option value="">Select an option</option>
<option value="Yes">Yes</option>
<option value="No">No</option>
@@ -241,6 +151,7 @@
>Can you provide a screenshot or screen recording?</label
>
<textarea
class="textarea"
id="screenshotInfo"
rows="2"
placeholder="Attach if possible (e.g., URL to screenshot)"
@@ -249,12 +160,14 @@
>Did the issue generate any error messages?</label
>
<textarea
class="textarea"
id="errorMessages"
rows="2"
placeholder="Include the exact error messages, if any"
></textarea>
<label for="consoleLogs">Any relevant console logs?</label>
<textarea
class="textarea"
id="consoleLogs"
rows="2"
placeholder="Paste any browser console logs if available"
@@ -263,7 +176,7 @@
<!-- Section 5: Additional Information -->
<h2>5. Additional Information</h2>
<label for="bugFrequency">How frequently does this bug occur?</label>
<select id="bugFrequency">
<select class="select" id="bugFrequency">
<option value="">Select an option</option>
<option value="Every time">Every time</option>
<option value="Occasionally">Occasionally</option>
@@ -273,7 +186,7 @@
>Does this bug impact core functionality (e.g., video playback,
uploads, etc.)?</label
>
<select id="impactCore">
<select class="select" id="impactCore">
<option value="">Select an option</option>
<option value="Yes">Yes</option>
<option value="No">No</option>
@@ -282,17 +195,28 @@
>Any additional notes or suggestions?</label
>
<textarea
class="textarea"
id="additionalNotes"
rows="3"
placeholder="Describe any other relevant details"
></textarea>
<button type="submit">Submit Bug Report</button>
<button class="btn" type="submit">Submit Bug Report</button>
</form>
<div id="status"></div>
</div>
</div>
<script>
// Provide no-op fallback loggers so embeds stay functional if bitvidLogger is unavailable.
const noopLogger = {
log: () => {},
info: () => {},
warn: () => {},
error: () => {},
};
const dev = window.bitvidLogger?.dev || noopLogger;
const user = window.bitvidLogger?.user || noopLogger;
document.addEventListener("DOMContentLoaded", () => {
// Logging functions for both on-page and console output.
function log(msg, type = "info") {
@@ -303,7 +227,18 @@
if (type === "warn") div.classList.add("warn");
div.textContent = msg;
document.getElementById("status").appendChild(div);
console.log(`[${type.toUpperCase()}] ${msg}`);
const formatted = `[${type.toUpperCase()}] ${msg}`;
if (type === "error") {
user.error(formatted);
} else if (type === "warn") {
user.warn(formatted);
} else if (type === "success") {
user.log(formatted);
} else if (type === "info") {
dev.info(formatted);
} else {
dev.log(formatted);
}
}
function clear() {
document.getElementById("status").innerHTML = "";
@@ -524,10 +459,11 @@ For urgent issues, contact us through bitvid’s Nostr support channels.
);
} catch (err) {
log("Error: " + err.message, "error");
console.error(err);
user.error(err);
}
});
});
</script>
<div id="uiOverlay" data-overlay-root aria-hidden="true"></div>
</body>
</html>
@@ -3,129 +3,13 @@
<head>
<meta charset="utf-8" />
<title>bitvid Content Appeals Form</title>
<script type="module" src="./theme-sync.js"></script>
<!-- Link to your main stylesheet -->
<link rel="stylesheet" href="../../css/style.css?v=2024.10.29" />
<style>
/* Override for form page to match modal field styling */
/* Remove width constraints from body so our container can be full width */
body {
background-color: transparent;
color: #fff;
font-family: system-ui, -apple-system, sans-serif;
margin: 20px;
}
/* Override the .container to use the full available width */
.container {
width: 100%;
max-width: 100%;
margin: 0;
padding: 0;
}
/* Card-like container for the form, similar to modal-content */
.form-container {
background-color: #111827; /* Tailwind's bg-gray-900 */
padding: 1.5rem;
border-radius: 0.5rem;
box-shadow: var(--shadow-md);
}
h1,
h2 {
color: #fff;
}
/* Labels in a light gray */
label {
display: block;
margin-top: 1em;
font-weight: bold;
color: #e5e7eb; /* Tailwind's text-gray-200 */
}
/* Input, textarea, and select mimic modal field styles */
input,
textarea,
select {
width: 100%;
margin-bottom: 0.75em;
background-color: #1f2937; /* Tailwind's bg-gray-800 */
color: #f3f4f6; /* Tailwind's text-gray-100 */
border: 1px solid #374151; /* Tailwind's border-gray-700 */
padding: 0.5em;
border-radius: 0.375rem; /* rounded-md */
box-sizing: border-box;
}
input:focus,
textarea:focus,
select:focus {
border-color: #3b82f6; /* blue-500 */
outline: none;
box-shadow: 0 0 0 1px #3b82f6;
}
/* Button styled similarly to modal publish button */
button {
padding: 0.5em 1em;
background: #3b82f6; /* blue-500 */
color: #fff;
border: none;
border-radius: 0.375rem;
cursor: pointer;
}
/* Status log area */
#status {
margin-top: 1em;
padding: 0.5em;
background: #111827;
white-space: pre-wrap;
min-height: 80px;
border-radius: 0.25rem;
}
.status-line {
margin: 0.25em 0;
}
.error {
color: #f87171; /* a red tint */
}
.success {
color: #3b82f6; /* blue-500 */
}
.warn {
color: #facc15; /* a yellow tone */
}
/* Custom Scrollbar styling for WebKit browsers */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
}
::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.3);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(255, 255, 255, 0.4);
}
/* Custom Scrollbar styling for Firefox */
* {
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
}
</style>
<link rel="stylesheet" href="../../css/tailwind.generated.css" />
<!-- Load nostr‑tools v2.10.4 -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/nostr.bundle.min.js"></script>
</head>
<body>
<body class="iframe-form" data-ds="new">
<div class="container">
<div class="form-container">
<p>
@@ -138,6 +22,7 @@
<h2>1. User Information</h2>
<label for="contactMethod">Contact Method (if applicable):</label>
<input
class="form-control"
type="text"
id="contactMethod"
placeholder="Nostr DM, email, or other"
@@ -146,6 +31,7 @@
<h2>2. Content Details</h2>
<label for="videoTitle">Title of the Video:</label>
<input
class="form-control"
type="text"
id="videoTitle"
placeholder="Enter the exact title"
@@ -153,19 +39,21 @@
<label for="magnetLink">Magnet Link:</label>
<input
class="form-control"
type="text"
id="magnetLink"
placeholder="Enter the magnet link"
/>
<label for="submissionDate">Date of Content Submission:</label>
<input type="date" id="submissionDate" />
<input class="form-control" type="date" id="submissionDate" />
<h2>3. Reason for Appeal</h2>
<label for="reasonBlocked"
>Why do you believe your content was unfairly blocked?</label
>
<textarea
class="textarea"
id="reasonBlocked"
rows="3"
placeholder="Explain in detail"
@@ -174,7 +62,7 @@
<label for="fitsGuidelines"
>Does your content fit within bitvid's Community Guidelines?</label
>
<select id="fitsGuidelines">
<select class="select" id="fitsGuidelines">
<option value="">Select an option</option>
<option value="Yes">Yes</option>
<option value="No">No</option>
@@ -184,6 +72,7 @@
>If yes, which guideline(s) support your appeal?</label
>
<textarea
class="textarea"
id="guidelinesCited"
rows="2"
placeholder="Cite the specific guidelines"
@@ -192,7 +81,7 @@
<label for="editedContent"
>Was this content edited after being blocked?</label
>
<select id="editedContent">
<select class="select" id="editedContent">
<option value="">Select an option</option>
<option value="Yes">Yes</option>
<option value="No">No</option>
@@ -200,6 +89,7 @@
<label for="changesMade">If yes, what changes were made?</label>
<textarea
class="textarea"
id="changesMade"
rows="2"
placeholder="Describe the modifications"
@@ -210,6 +100,7 @@
>Was there any misunderstanding or misclassification?</label
>
<textarea
class="textarea"
id="misunderstanding"
rows="2"
placeholder="Provide context"
@@ -219,6 +110,7 @@
>Are there external references that validate your appeal?</label
>
<textarea
class="textarea"
id="externalReferences"
rows="2"
placeholder="Links, citations, or additional info"
@@ -233,17 +125,32 @@
decisions are at the discretion of bitvid’s moderation process.
</p>
<label for="signature">Signature (Digital or Written):</label>
<input type="text" id="signature" placeholder="Your signature" />
<input
class="form-control"
type="text"
id="signature"
placeholder="Your signature"
/>
<label for="declarationDate">Date:</label>
<input type="date" id="declarationDate" />
<input class="form-control" type="date" id="declarationDate" />
<button type="submit">Submit Appeal</button>
<button class="btn" type="submit">Submit Appeal</button>
</form>
<div id="status"></div>
</div>
</div>
<script>
// Provide no-op fallback loggers so embeds stay functional if bitvidLogger is unavailable.
const noopLogger = {
log: () => {},
info: () => {},
warn: () => {},
error: () => {},
};
const dev = window.bitvidLogger?.dev || noopLogger;
const user = window.bitvidLogger?.user || noopLogger;
document.addEventListener("DOMContentLoaded", () => {
// Logging functions for both on-page and console output.
function log(msg, type = "info") {
@@ -254,7 +161,18 @@
if (type === "warn") div.classList.add("warn");
div.textContent = msg;
document.getElementById("status").appendChild(div);
console.log(`[${type.toUpperCase()}] ${msg}`);
const formatted = `[${type.toUpperCase()}] ${msg}`;
if (type === "error") {
user.error(formatted);
} else if (type === "warn") {
user.warn(formatted);
} else if (type === "success") {
user.log(formatted);
} else if (type === "info") {
dev.info(formatted);
} else {
dev.log(formatted);
}
}
function clear() {
document.getElementById("status").innerHTML = "";
@@ -457,10 +375,11 @@ For further questions, reach out through bitvid’s Nostr support channels.
);
} catch (err) {
log("Error: " + err.message, "error");
console.error(err);
user.error(err);
}
});
});
</script>
<div id="uiOverlay" data-overlay-root aria-hidden="true"></div>
</body>
</html>
+48 -139
View File
@@ -3,143 +3,13 @@
<head>
<meta charset="utf-8" />
<title>bitvid General Feedback Form</title>
<script type="module" src="./theme-sync.js"></script>
<!-- Link to your main stylesheet -->
<link rel="stylesheet" href="../../css/style.css?v=2024.10.29" />
<style>
/* Override for form page to match modal field styling */
/* Remove width constraints from body so our container can be full width */
body {
background-color: transparent;
color: #fff;
font-family: system-ui, -apple-system, sans-serif;
margin: 20px;
}
/* Override the .container to use the full available width */
.container {
width: 100%;
max-width: 100%;
margin: 0;
padding: 0;
}
/* Card-like container for the form */
.form-container {
background-color: #111827; /* Tailwind's bg-gray-900 */
padding: 1.5rem;
border-radius: 0.5rem;
box-shadow: var(--shadow-md);
}
h1,
h2 {
color: #fff;
}
/* Labels in a light gray */
label {
display: block;
margin-top: 1em;
font-weight: bold;
color: #e5e7eb; /* Tailwind's text-gray-200 */
}
/* Input, textarea, and select styling */
input,
textarea,
select {
width: 100%;
margin-bottom: 0.75em;
background-color: #1f2937; /* Tailwind's bg-gray-800 */
color: #f3f4f6; /* Tailwind's text-gray-100 */
border: 1px solid #374151; /* Tailwind's border-gray-700 */
padding: 0.5em;
border-radius: 0.375rem; /* rounded-md */
box-sizing: border-box;
}
input:focus,
textarea:focus,
select:focus {
border-color: #3b82f6; /* blue-500 */
outline: none;
box-shadow: 0 0 0 1px #3b82f6;
}
/* Checkbox group styling */
.checkbox-group label {
display: inline-block;
margin-right: 1rem;
font-weight: normal;
}
/* Radio group styling (for experience rating) */
.radio-group label {
display: inline-block;
margin-right: 1rem;
font-weight: normal;
}
/* Button styling */
button {
padding: 0.5em 1em;
background: #3b82f6; /* blue-500 */
color: #fff;
border: none;
border-radius: 0.375rem;
cursor: pointer;
}
/* Status log area */
#status {
margin-top: 1em;
padding: 0.5em;
background: #111827;
white-space: pre-wrap;
min-height: 80px;
border-radius: 0.25rem;
}
.status-line {
margin: 0.25em 0;
}
.error {
color: #f87171;
}
.success {
color: #3b82f6;
}
.warn {
color: #facc15;
}
/* Custom Scrollbar styling for WebKit browsers */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
}
::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.3);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(255, 255, 255, 0.4);
}
/* Custom Scrollbar styling for Firefox */
* {
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
}
</style>
<link rel="stylesheet" href="../../css/tailwind.generated.css" />
<!-- Load nostr‑tools v2.10.4 -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/nostr.bundle.min.js"></script>
</head>
<body>
<body class="iframe-form" data-ds="new">
<div class="container">
<div class="form-container">
<p>
@@ -152,6 +22,7 @@
<h2>1. User Information</h2>
<label for="userNpub">Nostr Public Key (npub) (optional):</label>
<input
class="form-control"
type="text"
id="userNpub"
placeholder="Enter your npub (optional)"
@@ -159,15 +30,26 @@
<p>Are you a (check all that apply):</p>
<div class="checkbox-group">
<label
><input type="checkbox" name="userRole" value="Viewer" />
><input
class="checkbox"
type="checkbox"
name="userRole"
value="Viewer"
/>
Viewer</label
>
<label
><input type="checkbox" name="userRole" value="Content Creator" />
><input
class="checkbox"
type="checkbox"
name="userRole"
value="Content Creator"
/>
Content Creator</label
>
<label
><input
class="checkbox"
type="checkbox"
name="userRole"
value="Developer/Contributor"
@@ -203,12 +85,14 @@
</div>
<label for="likeMost">What do you like most about bitvid?</label>
<textarea
class="textarea"
id="likeMost"
rows="3"
placeholder="Describe the features, usability, or content you enjoy"
></textarea>
<label for="improvements">What would you like to see improved?</label>
<textarea
class="textarea"
id="improvements"
rows="3"
placeholder="Provide specific areas for improvement"
@@ -218,6 +102,7 @@
use?</label
>
<textarea
class="textarea"
id="confusingFeatures"
rows="3"
placeholder="Explain any challenges you’ve encountered"
@@ -229,6 +114,7 @@
>Do you have any other suggestions or thoughts about bitvid?</label
>
<textarea
class="textarea"
id="otherSuggestions"
rows="3"
placeholder="Share any other feedback"
@@ -236,7 +122,7 @@
<label for="followUp"
>Would you like to be contacted for follow-up discussions?</label
>
<select id="followUp">
<select class="select" id="followUp">
<option value="">Select an option</option>
<option value="Yes">Yes</option>
<option value="No">No</option>
@@ -245,17 +131,28 @@
>Preferred contact method (if applicable):</label
>
<input
class="form-control"
type="text"
id="preferredContact"
placeholder="Nostr DM, email, or other"
/>
<button type="submit">Submit General Feedback</button>
<button class="btn" type="submit">Submit General Feedback</button>
</form>
<div id="status"></div>
</div>
</div>
<script>
// Provide no-op fallback loggers so embeds stay functional if bitvidLogger is unavailable.
const noopLogger = {
log: () => {},
info: () => {},
warn: () => {},
error: () => {},
};
const dev = window.bitvidLogger?.dev || noopLogger;
const user = window.bitvidLogger?.user || noopLogger;
document.addEventListener("DOMContentLoaded", () => {
// Logging function for on-page and console output.
function log(msg, type = "info") {
@@ -266,7 +163,18 @@
if (type === "warn") div.classList.add("warn");
div.textContent = msg;
document.getElementById("status").appendChild(div);
console.log(`[${type.toUpperCase()}] ${msg}`);
const formatted = `[${type.toUpperCase()}] ${msg}`;
if (type === "error") {
user.error(formatted);
} else if (type === "warn") {
user.warn(formatted);
} else if (type === "success") {
user.log(formatted);
} else if (type === "info") {
dev.info(formatted);
} else {
dev.log(formatted);
}
}
function clear() {
document.getElementById("status").innerHTML = "";
@@ -450,10 +358,11 @@ For additional discussions, reach out via bitvid’s Nostr support channels.
);
} catch (err) {
log("Error: " + err.message, "error");
console.error(err);
user.error(err);
}
});
});
</script>
<div id="uiOverlay" data-overlay-root aria-hidden="true"></div>
</body>
</html>
+69 -132
View File
@@ -3,133 +3,13 @@
<head>
<meta charset="utf-8" />
<title>bitvid Feature Request Form</title>
<script type="module" src="./theme-sync.js"></script>
<!-- Link to your main stylesheet -->
<link rel="stylesheet" href="../../css/style.css?v=2024.10.29" />
<style>
/* Override for form page to match modal field styling */
/* Remove width constraints from body so our container can be full width */
body {
background-color: transparent;
color: #fff;
font-family: system-ui, -apple-system, sans-serif;
margin: 20px;
}
/* Full-width container */
.container {
width: 100%;
max-width: 100%;
margin: 0;
padding: 0;
}
/* Card-like container for the form */
.form-container {
background-color: #111827;
padding: 1.5rem;
border-radius: 0.5rem;
box-shadow: var(--shadow-md);
}
h1,
h2 {
color: #fff;
}
/* Labels */
label {
display: block;
margin-top: 1em;
font-weight: bold;
color: #e5e7eb;
}
/* Input, textarea, and select styling */
input,
textarea,
select {
width: 100%;
margin-bottom: 0.75em;
background-color: #1f2937;
color: #f3f4f6;
border: 1px solid #374151;
padding: 0.5em;
border-radius: 0.375rem;
box-sizing: border-box;
}
input:focus,
textarea:focus,
select:focus {
border-color: #3b82f6;
outline: none;
box-shadow: 0 0 0 1px #3b82f6;
}
/* Checkbox group styling */
.checkbox-group label {
display: inline-block;
margin-right: 1rem;
font-weight: normal;
}
/* Button styling */
button {
padding: 0.5em 1em;
background: #3b82f6;
color: #fff;
border: none;
border-radius: 0.375rem;
cursor: pointer;
}
/* Status log area */
#status {
margin-top: 1em;
padding: 0.5em;
background: #111827;
white-space: pre-wrap;
min-height: 80px;
border-radius: 0.25rem;
}
.status-line {
margin: 0.25em 0;
}
.error {
color: #f87171;
}
.success {
color: #3b82f6;
}
.warn {
color: #facc15;
}
/* Custom Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
}
::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.3);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(255, 255, 255, 0.4);
}
* {
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
}
</style>
<link rel="stylesheet" href="../../css/tailwind.generated.css" />
<!-- Load nostr‑tools v2.10.4 -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/nostr.bundle.min.js"></script>
</head>
<body>
<body class="iframe-form" data-ds="new">
<div class="container">
<div class="form-container">
<p>
@@ -142,6 +22,7 @@
<h2>1. User Information</h2>
<label for="userNpub">Nostr Public Key (npub) (optional):</label>
<input
class="form-control"
type="text"
id="userNpub"
placeholder="Enter your npub (optional)"
@@ -149,15 +30,26 @@
<p>Are you a (check all that apply):</p>
<div class="checkbox-group">
<label
><input type="checkbox" name="userRole" value="Viewer" />
><input
class="checkbox"
type="checkbox"
name="userRole"
value="Viewer"
/>
Viewer</label
>
<label
><input type="checkbox" name="userRole" value="Content Creator" />
><input
class="checkbox"
type="checkbox"
name="userRole"
value="Content Creator"
/>
Content Creator</label
>
<label
><input
class="checkbox"
type="checkbox"
name="userRole"
value="Developer/Contributor"
@@ -170,6 +62,7 @@
<h2>2. Feature Request Details</h2>
<label for="featureName">Feature Name:</label>
<input
class="form-control"
type="text"
id="featureName"
placeholder="Short, descriptive title"
@@ -177,18 +70,21 @@
/>
<label for="featureDescription">Describe the feature:</label>
<textarea
class="textarea"
id="featureDescription"
rows="3"
placeholder="Explain in detail what the feature does and how it works"
></textarea>
<label for="featureImportance">Why is this feature important?</label>
<textarea
class="textarea"
id="featureImportance"
rows="2"
placeholder="Describe how this will improve the platform and benefit users"
></textarea>
<label for="beneficiary">Who would benefit from this feature?</label>
<input
class="form-control"
type="text"
id="beneficiary"
placeholder="e.g., Content Creators, Viewers, Both"
@@ -200,12 +96,14 @@
>Are there existing platforms that have this feature?</label
>
<textarea
class="textarea"
id="existingPlatforms"
rows="2"
placeholder="Provide examples if applicable"
></textarea>
<label for="mockups">Do you have any mockups or examples?</label>
<textarea
class="textarea"
id="mockups"
rows="2"
placeholder="Links, screenshots, or diagrams"
@@ -214,7 +112,7 @@
>Would you be willing to help test this feature if
implemented?</label
>
<select id="willingToTest">
<select class="select" id="willingToTest">
<option value="">Select an option</option>
<option value="Yes">Yes</option>
<option value="No">No</option>
@@ -225,15 +123,31 @@
<p>How urgent is this feature?</p>
<div class="checkbox-group">
<label
><input type="checkbox" name="featurePriority" value="High" />
><input
class="checkbox"
type="checkbox"
name="featurePriority"
value="High"
/>
High (Essential for platform success)</label
>
<label
><input type="checkbox" name="featurePriority" value="Medium" />
><input
class="checkbox"
type="checkbox"
name="featurePriority"
value="Medium"
/>
Medium (Would improve experience significantly)</label
>
<label
><input type="checkbox" name="featurePriority" value="Low" /> Low
><input
class="checkbox"
type="checkbox"
name="featurePriority"
value="Low"
/>
Low
(Nice to have, but not urgent)</label
>
</div>
@@ -241,17 +155,28 @@
>Does this feature require technical expertise to implement?</label
>
<textarea
class="textarea"
id="techChallenges"
rows="2"
placeholder="Describe any dependencies or challenges"
></textarea>
<button type="submit">Submit Feature Request</button>
<button class="btn" type="submit">Submit Feature Request</button>
</form>
<div id="status"></div>
</div>
</div>
<script>
// Provide no-op fallback loggers so embeds stay functional if bitvidLogger is unavailable.
const noopLogger = {
log: () => {},
info: () => {},
warn: () => {},
error: () => {},
};
const dev = window.bitvidLogger?.dev || noopLogger;
const user = window.bitvidLogger?.user || noopLogger;
document.addEventListener("DOMContentLoaded", () => {
// Logging function
function log(msg, type = "info") {
@@ -262,7 +187,18 @@
if (type === "warn") div.classList.add("warn");
div.textContent = msg;
document.getElementById("status").appendChild(div);
console.log(`[${type.toUpperCase()}] ${msg}`);
const formatted = `[${type.toUpperCase()}] ${msg}`;
if (type === "error") {
user.error(formatted);
} else if (type === "warn") {
user.warn(formatted);
} else if (type === "success") {
user.log(formatted);
} else if (type === "info") {
dev.info(formatted);
} else {
dev.log(formatted);
}
}
function clear() {
document.getElementById("status").innerHTML = "";
@@ -465,10 +401,11 @@ For further discussions, reach out through bitvid’s Nostr support channels.
);
} catch (err) {
log("Error: " + err.message, "error");
console.error(err);
user.error(err);
}
});
});
</script>
<div id="uiOverlay" data-overlay-root aria-hidden="true"></div>
</body>
</html>
+192
View File
@@ -0,0 +1,192 @@
import { THEME_ACCENT_OVERRIDES } from "../../config/instance-config.js";
(() => {
const VALID_THEMES = new Set(["light", "dark"]);
const STORAGE_KEY = "bitvid:theme";
const FALLBACK_THEME = "dark";
const ACCENT_CSS_VARIABLES = Object.freeze({
accent: "--color-accent",
accentStrong: "--color-accent-strong",
accentPressed: "--color-accent-pressed",
});
const ACCENT_TOKENS = Object.keys(ACCENT_CSS_VARIABLES);
const { documentElement: root } = document;
if (!(root instanceof HTMLElement)) {
return;
}
const sanitizeConfigOverrides = (overrides) => {
if (!overrides || typeof overrides !== "object") {
return {};
}
const sanitized = {};
Object.entries(overrides).forEach(([theme, themeOverrides]) => {
if (!VALID_THEMES.has(theme) || typeof themeOverrides !== "object") {
return;
}
const normalized = {};
ACCENT_TOKENS.forEach((token) => {
const value = themeOverrides[token];
if (typeof value === "string") {
const trimmed = value.trim();
if (trimmed !== "") {
normalized[token] = trimmed;
}
}
});
if (Object.keys(normalized).length > 0) {
sanitized[theme] = normalized;
}
});
return sanitized;
};
const CONFIG_ACCENT_OVERRIDES = sanitizeConfigOverrides(
THEME_ACCENT_OVERRIDES
);
const normalizeTheme = (value) => {
if (typeof value !== "string") {
return null;
}
const normalized = value.trim().toLowerCase();
if (normalized === "" || normalized === "default") {
return FALLBACK_THEME;
}
return VALID_THEMES.has(normalized) ? normalized : null;
};
const getParentRoot = () => {
try {
if (window.parent && window.parent !== window) {
const parentDocument = window.parent.document;
const parentRoot = parentDocument?.documentElement;
return parentRoot instanceof HTMLElement ? parentRoot : null;
}
} catch (error) {
return null;
}
return null;
};
const readParentTheme = () => {
const parentRoot = getParentRoot();
if (!parentRoot) {
return null;
}
return normalizeTheme(parentRoot.dataset?.theme);
};
const readStoredTheme = () => {
try {
return normalizeTheme(window.localStorage.getItem(STORAGE_KEY));
} catch (error) {
return null;
}
};
const applyTheme = (theme) => {
if (theme && VALID_THEMES.has(theme)) {
root.dataset.theme = theme;
} else {
root.removeAttribute("data-theme");
}
};
const readParentAccentOverrides = () => {
const parentRoot = getParentRoot();
if (!parentRoot) {
return null;
}
const overrides = {};
let hasOverride = false;
Object.values(ACCENT_CSS_VARIABLES).forEach((cssVariable) => {
try {
const value = parentRoot.style.getPropertyValue(cssVariable);
if (typeof value === "string") {
const trimmed = value.trim();
if (trimmed !== "") {
overrides[cssVariable] = trimmed;
hasOverride = true;
}
}
} catch (error) {
/* noop */
}
});
return hasOverride ? overrides : null;
};
const applyAccentOverrides = (theme) => {
const parentOverrides = readParentAccentOverrides();
const configOverrides = CONFIG_ACCENT_OVERRIDES[theme] || null;
ACCENT_TOKENS.forEach((token) => {
const cssVariable = ACCENT_CSS_VARIABLES[token];
const parentValue = parentOverrides?.[cssVariable];
const configValue = configOverrides?.[token];
const nextValue = parentValue || configValue || "";
if (typeof nextValue === "string" && nextValue.trim() !== "") {
root.style.setProperty(cssVariable, nextValue.trim());
} else {
root.style.removeProperty(cssVariable);
}
});
};
const syncTheme = () => {
const resolvedTheme =
readParentTheme() ?? readStoredTheme() ?? FALLBACK_THEME;
applyTheme(resolvedTheme);
applyAccentOverrides(resolvedTheme);
};
syncTheme();
const parentRoot = getParentRoot();
let observer;
if (parentRoot && typeof MutationObserver !== "undefined") {
observer = new MutationObserver(syncTheme);
observer.observe(parentRoot, {
attributes: true,
attributeFilter: ["data-theme", "style"],
});
}
window.addEventListener("storage", (event) => {
if (event?.key === STORAGE_KEY) {
syncTheme();
}
});
window.addEventListener(
"pageshow",
() => {
syncTheme();
},
{ once: true }
);
window.addEventListener(
"beforeunload",
() => {
if (observer) {
observer.disconnect();
}
},
{ once: true }
);
})();
+80
View File
@@ -0,0 +1,80 @@
<div
id="lockdownModal"
class="bv-modal hidden items-center justify-center"
role="dialog"
aria-modal="true"
aria-labelledby="lockdownModalTitle"
aria-describedby="lockdownModalDescription"
data-ds="new"
>
<div class="bv-modal-backdrop"></div>
<div class="modal-sheet max-h-modal-shell overflow-hidden" tabindex="-1">
<div class="modal-body space-y-8 text-center">
<div class="flex justify-center">
<span class="inline-flex h-16 items-center text-text bv-logo" role="img" aria-label="bitvid logo">
<svg class="h-full w-auto" viewBox="0 0 318.32 108.98" aria-hidden="true" focusable="false">
<g class="bv-logo__accent">
<rect x="7.1" y="7.8" width="5.84" height="5.48" />
<rect x="7.1" y="18.78" width="24.02" height="5.48" />
<rect x="7.1" y="29.77" width="42.85" height="5.48" />
<rect x="7.1" y="40.76" width="61.68" height="5.48" />
<rect x="7.1" y="51.75" width="80.88" height="5.48" />
<rect x="7.1" y="62.74" width="61.68" height="5.48" />
<rect x="7.1" y="73.73" width="42.85" height="5.48" />
<rect x="7.1" y="84.72" width="24.02" height="5.48" />
<rect x="7.1" y="95.71" width="5.84" height="5.48" />
</g>
<g class="bv-logo__wordmark">
<path d="M121.28 25.08v44.54c0 5.67 2.79 8.54 8.22 8.54h11.57c5.51 0 8.3-2.87 8.3-8.54v-13.09c0-5.67-2.79-8.46-8.3-8.46h-16.6v-5.83h17.72c8.7 0 13.01 4.47 13.01 13.33v15.01c0 8.86-4.31 13.33-13.01 13.33h-13.73c-8.7 0-13.01-4.47-13.01-13.33V25.08h5.83Z" />
<path d="M168.53 25.08v6.26h-5.83v-6.26h5.83Zm0 17.56v41.27h-5.83v-41.27h5.83Z" />
<path d="M191.36 25.08v17.16h9.02v5.19h-9.02v36.48h-5.83v-36.48h-9.82v-5.19h9.82V25.08h5.83Z" />
<path d="M212.88 42.24l16.84 35.68 16.6-35.68h6.07l-19.56 41.67h-6.31l-20.04-41.67h6.39Z" />
<path d="M264.05 25.08v6.26h-5.83v-6.26h5.83Zm0 17.16v41.67h-5.83v-41.67h5.83Z" />
<path d="M311.21 25.08v45.5c0 8.86-4.31 13.33-13.01 13.33h-13.73c-8.7 0-13.01-4.47-13.01-13.33v-15.01c0-8.86 4.31-13.33 13.01-13.33h17.72v5.83h-16.6c-5.51 0-8.3 2.79-8.3 8.46v13.09c0 5.67 2.79 8.54 8.3 8.54h11.49c5.51 0 8.3-2.87 8.3-8.54V25.08h5.83Z" />
</g>
</svg>
</span>
</div>
<div id="lockdownModalDescription" class="space-y-4">
<h2 id="lockdownModalTitle" class="text-2xl font-semibold tracking-tight text-text">
bitvid is in lockdown mode
</h2>
<p class="text-base leading-relaxed text-text-frosted">
Only approved administrators and moderators can access the app while lockdown is active. Sign in with an authorized Nostr profile to continue.
</p>
<p class="text-sm text-text-muted">
You will be prompted by your NIP-07 browser extension. Make sure it is installed, unlocked, and ready to approve the request.
</p>
</div>
<div class="space-y-2">
<p
id="lockdownStatusMessage"
class="text-sm text-text"
data-lockdown-status
aria-live="polite"
>
Administrator access is required to proceed.
</p>
<p
id="lockdownErrorMessage"
class="hidden text-sm text-critical"
data-lockdown-error
role="alert"
></p>
</div>
<button
id="lockdownLoginButton"
class="btn w-full"
data-variant="danger"
data-lockdown-login
type="button"
>
Login with Nostr
</button>
</div>
</div>
</div>
+59 -67
View File
@@ -1,79 +1,71 @@
<div
id="loginModal"
class="fixed inset-0 z-50 hidden"
style="background: transparent"
class="bv-modal hidden items-start justify-center"
role="dialog"
aria-modal="true"
aria-labelledby="loginModalTitle"
aria-describedby="loginModalDescription"
data-ds="new"
>
<!-- Dark overlay with blur (unchanged) -->
<div
class="absolute inset-0 z-10"
style="
background-color: rgba(0, 0, 0, 0.9);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
"
></div>
<div class="bv-modal-backdrop" data-dismiss></div>
<!-- Outer container matching video modal's flex layout & wide sizing -->
<div
class="modal-container relative h-full w-full flex items-start justify-center overflow-y-auto z-20"
>
<div
class="modal-content bg-gray-900 w-full max-w-[90%] lg:max-w-6xl my-0 rounded-lg overflow-hidden relative"
>
<!-- Header with the special exit button (unchanged) -->
<div
class="sticky top-0 bg-gradient-to-b from-black/80 to-transparent p-4 flex items-center justify-between"
<div class="modal-sheet max-h-modal-shell overflow-hidden" tabindex="-1">
<!-- Header with the special exit button (unchanged) -->
<div class="modal-header">
<h2 id="loginModalTitle" class="text-xl font-bold text-text">
Login Options
</h2>
<button
id="closeLoginModal"
class="modal-close"
>
<h2 class="text-xl font-bold text-white">Login Options</h2>
<button
id="closeLoginModal"
class="flex items-center justify-center w-10 h-10 rounded-full bg-black/50 hover:bg-black/70 transition-all duration-200 backdrop-blur focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-black focus:ring-blue-500"
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-gray-300"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<!-- Body with buttons (unchanged) -->
<div class="p-6 space-y-4">
<!-- Application Form button -->
<button
id="openApplicationModal"
class="w-full bg-indigo-500 text-white px-4 py-2 rounded-md hover:bg-indigo-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
Application Form
</button>
<!-- Body with buttons (unchanged) -->
<div id="loginModalDescription" class="modal-body space-y-4 overflow-y-auto">
<!-- Application Form button -->
<button
id="openApplicationModal"
class="btn w-full"
type="button"
>
Application Form
</button>
<!-- Simple horizontal rule -->
<div class="my-2 border-t border-gray-700"></div>
<!-- Simple horizontal rule -->
<div class="my-2 border-t border-border/60"></div>
<!-- Existing login buttons -->
<button
id="loginNIP07"
class="w-full bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
>
Login with Extension (NIP-07)
</button>
<button
id="loginNSEC"
class="w-full bg-gray-500 text-white px-4 py-2 rounded-md cursor-not-allowed"
disabled
>
Login with NSEC (Coming Soon)
</button>
</div>
<!-- Existing login buttons -->
<button
id="loginNIP07"
class="btn w-full"
type="button"
>
Login with Extension (NIP-07)
</button>
<button
id="loginNSEC"
class="btn-ghost w-full"
disabled
type="button"
>
Login with NSEC (Coming Soon)
</button>
</div>
</div>
</div>
+327 -190
View File
@@ -1,44 +1,53 @@
<!-- components/profile-modal.html -->
<div
id="profileModal"
class="fixed inset-0 z-50 hidden"
class="bv-modal hidden items-start"
role="dialog"
aria-modal="true"
aria-labelledby="profileModalTitle"
tabindex="-1"
style="background: transparent"
data-ds="new"
data-mobile-view="menu"
>
<!-- Overlay Layer with dark background and blur -->
<div
class="absolute inset-0 z-10"
style="
background-color: rgba(0, 0, 0, 0.9);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
"
></div>
<div class="bv-modal-backdrop"></div>
<!-- Outer container with the same sizing approach as your other modals -->
<div
class="modal-container relative h-full w-full flex items-start justify-center overflow-y-auto z-20"
class="bv-modal__panel modal-sheet profile-modal"
tabindex="-1"
data-mobile-view="menu"
>
<!-- Inner content: wide layout, matching your video modal -->
<div
class="modal-content bg-gray-900 w-full max-w-[90%] lg:max-w-6xl my-0 rounded-lg overflow-hidden relative flex flex-col"
style="max-height: 90vh"
>
<div class="profile-modal__content">
<!-- Modal Header -->
<div
class="sticky top-0 bg-gradient-to-b from-black/80 to-transparent p-4 flex items-center justify-between"
>
<h2 id="profileModalTitle" class="text-xl font-bold text-white">Profile</h2>
<button
id="closeProfileModal"
class="flex items-center justify-center w-10 h-10 rounded-full bg-black/50 hover:bg-black/70 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-black focus:ring-blue-500"
>
<div class="modal-header profile-modal__header">
<div class="profile-modal__header-title">
<button
id="profileModalBack"
type="button"
class="modal-back btn-ghost focus-ring hidden"
aria-hidden="true"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 19l-7-7 7-7"
/>
</svg>
<span class="text-xs font-medium">Menu</span>
</button>
<h2 id="profileModalTitle" class="text-lg font-bold text-text">Profile</h2>
</div>
<button id="closeProfileModal" class="modal-close">
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-gray-300"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
@@ -54,71 +63,80 @@
</div>
<!-- Modal Body -->
<div class="p-6 flex-1 overflow-hidden">
<div class="flex flex-col lg:flex-row gap-6 h-full">
<div class="profile-modal__body">
<div
class="profile-modal__layout"
data-profile-layout
data-mobile-view="menu"
>
<!-- Left navigation column -->
<nav
class="flex lg:flex-col gap-2 flex-shrink-0"
aria-label="Profile sections"
>
<button
id="profileNavAccount"
type="button"
class="profile-nav-button px-4 py-2 rounded-md text-sm font-medium text-white bg-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-900"
aria-controls="profilePaneAccount"
aria-selected="true"
>
Account
</button>
<button
id="profileNavRelays"
type="button"
class="profile-nav-button px-4 py-2 rounded-md text-sm font-medium text-gray-400 hover:text-white hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-900"
aria-controls="profilePaneRelays"
aria-selected="false"
>
Relays
</button>
<button
id="profileNavWallet"
type="button"
class="profile-nav-button px-4 py-2 rounded-md text-sm font-medium text-gray-400 hover:text-white hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-900"
aria-controls="profilePaneWallet"
aria-selected="false"
>
Wallet
</button>
<button
id="profileNavBlocked"
type="button"
class="profile-nav-button px-4 py-2 rounded-md text-sm font-medium text-gray-400 hover:text-white hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-900"
aria-controls="profilePaneBlocked"
aria-selected="false"
>
Blocked
</button>
<button
id="profileNavHistory"
type="button"
class="profile-nav-button px-4 py-2 rounded-md text-sm font-medium text-gray-400 hover:text-white hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-900"
aria-controls="profilePaneHistory"
aria-selected="false"
>
Watch history
</button>
<button
id="profileNavAdmin"
type="button"
class="profile-nav-button px-4 py-2 rounded-md text-sm font-medium text-gray-400 hover:text-white hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-900 hidden"
aria-controls="profilePaneAdmin"
aria-selected="false"
>
Admin (Moderation)
</button>
</nav>
<div class="profile-modal__menu" data-profile-mobile-menu aria-hidden="false">
<nav class="profile-modal__nav" aria-label="Profile sections">
<button
id="profileNavAccount"
type="button"
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
aria-controls="profilePaneAccount"
aria-selected="true"
data-state="active"
>
Account
</button>
<button
id="profileNavRelays"
type="button"
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
aria-controls="profilePaneRelays"
aria-selected="false"
>
Relays
</button>
<button
id="profileNavWallet"
type="button"
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
aria-controls="profilePaneWallet"
aria-selected="false"
>
Wallet
</button>
<button
id="profileNavBlocked"
type="button"
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
aria-controls="profilePaneBlocked"
aria-selected="false"
>
Blocked
</button>
<button
id="profileNavHistory"
type="button"
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
aria-controls="profilePaneHistory"
aria-selected="false"
>
Watch history
</button>
<button
id="profileNavAdmin"
type="button"
class="btn-ghost focus-ring hidden w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
aria-controls="profilePaneAdmin"
aria-selected="false"
>
Admin (Moderation)
</button>
</nav>
</div>
<!-- Right content panes -->
<div class="flex-1 min-h-[320px] overflow-y-auto pr-1">
<div
class="profile-modal__pane-wrapper"
data-profile-mobile-pane
aria-hidden="true"
>
<div class="profile-modal__panes">
<section
id="profilePaneAccount"
class="profile-pane space-y-6"
@@ -126,65 +144,72 @@
aria-labelledby="profileNavAccount"
>
<!--
The account pane now relies on bespoke CSS (see css/style.css under
"Profile modal account pane") so we are not dependent on utility
The account pane now relies on bespoke CSS (see css/tailwind.source.css under
the "profile modal" section) so we are not dependent on utility
classes that may be purged or overridden by other builds.
-->
<div class="profile-switcher" id="profileSwitcherTray">
<div class="profile-switcher__header">
<div class="profile-switcher__intro">
<h3 class="profile-switcher__title">Switch profiles</h3>
<p class="profile-switcher__subtitle">
Save multiple Nostr profiles so you can swap identities without leaving BitVid.
<div id="profileSwitcherTray" class="card space-y-5 p-5">
<div class="flex flex-wrap items-start gap-4">
<div class="space-y-2">
<h3 class="text-base font-semibold text-text">Switch profiles</h3>
<p class="text-sm text-muted">
Save multiple Nostr profiles so you can swap identities without leaving bitvid.
</p>
</div>
<span class="profile-switcher__badge">Preview</span>
</div>
<div
class="profile-card profile-card--active"
class="card profile-modal__active-profile"
aria-label="Current Nostr profile"
aria-pressed="true"
role="button"
tabindex="0"
data-state="active"
>
<span class="profile-card__avatar">
<span
class="flex h-12 w-12 flex-shrink-0 items-center justify-center overflow-hidden rounded-full border-2 border-info/50 bg-surface"
>
<img
id="profileModalAvatar"
src="assets/svg/default-profile.svg"
alt="Active profile avatar"
class="h-full w-full object-cover"
/>
</span>
<span class="profile-card__meta">
<span class="profile-card__topline">
<span class="profile-card__label">Active profile</span>
<a
id="profileChannelLink"
href="#"
class="profile-card__action hidden"
>
View channel
</a>
</span>
<span id="profileModalName" class="profile-card__name">Loading...</span>
<span id="profileModalNpub" class="profile-card__npub">npub1...</span>
</span>
<div class="profile-modal__active-details">
<div class="profile-modal__active-meta">
<span class="text-3xs font-semibold uppercase tracking-extra-wide text-info">
Active profile
</span>
</div>
<span id="profileModalName" class="truncate text-sm font-semibold text-text">Loading...</span>
<span id="profileModalNpub" class="break-all font-mono text-xs text-muted">npub1...</span>
</div>
<a
id="profileChannelLink"
href="#"
class="btn-ghost focus-ring profile-modal__channel-link hidden text-xs"
>
View channel
</a>
</div>
<div
id="profileSwitcherList"
class="profile-switcher__list"
class="grid-stack"
aria-live="polite"
aria-label="Additional Nostr profiles"
role="list"
data-orientation="vertical"
></div>
<button
type="button"
id="profileAddAccountBtn"
class="profile-switcher__add"
class="profile-modal__add-profile"
aria-label="Add a new Nostr profile"
>
<span class="profile-switcher__addAvatar">
<span class="profile-modal__add-profile-icon" aria-hidden="true">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
@@ -193,22 +218,131 @@
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
</span>
<span class="profile-switcher__addCopy">
<span class="profile-switcher__addTitle">Add profile</span>
<span class="profile-switcher__addHint">Link another npub</span>
<span class="flex flex-col text-left">
<span data-profile-add-title class="text-sm font-semibold text-text">Add profile</span>
<span data-profile-add-hint class="text-xs text-muted-strong">Link another npub</span>
</span>
</button>
<p class="profile-switcher__helper">
<p class="text-sm text-muted">
Profiles you add here will stay on this device so you can quickly switch before posting or moderating.
</p>
</div>
<div id="profileModerationSettings" class="card space-y-5 p-5">
<div class="space-y-2">
<h3 class="text-base font-semibold text-text">Safety & Moderation</h3>
<p class="text-sm text-muted">
Choose how many trusted reports or mutes it takes to blur thumbnails, block autoplay previews, or hide videos entirely.
Lower numbers are stricter.
</p>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<label
class="block text-sm text-muted-strong"
for="profileModerationBlurThreshold"
>
<span class="mb-2 block font-medium">Blur after trusted reports</span>
<input
id="profileModerationBlurThreshold"
type="number"
inputmode="numeric"
min="0"
step="1"
class="input"
/>
<span class="mt-1 block text-xs text-muted">
Thumbnails blur when trusted contacts report nudity at or above this count.
</span>
</label>
<label
class="block text-sm text-muted-strong"
for="profileModerationAutoplayThreshold"
>
<span class="mb-2 block font-medium">Block autoplay after reports</span>
<input
id="profileModerationAutoplayThreshold"
type="number"
inputmode="numeric"
min="0"
step="1"
class="input"
/>
<span class="mt-1 block text-xs text-muted">
Autoplay previews pause once trusted reports meet this threshold.
</span>
</label>
</div>
<div
class="grid gap-4 sm:grid-cols-2"
data-role="trusted-hide-controls"
>
<label
class="block text-sm text-muted-strong"
for="profileModerationMuteHideThreshold"
data-role="trusted-hide-control"
>
<span class="mb-2 block font-medium">Hide after trusted mutes</span>
<input
id="profileModerationMuteHideThreshold"
type="number"
inputmode="numeric"
min="0"
step="1"
class="input"
/>
<span class="mt-1 block text-xs text-muted">
Hide videos when this many trusted contacts add the author to their mute list.
</span>
</label>
<label
class="block text-sm text-muted-strong"
for="profileModerationSpamHideThreshold"
data-role="trusted-hide-control"
>
<span class="mb-2 block font-medium">Hide after trusted spam reports</span>
<input
id="profileModerationSpamHideThreshold"
type="number"
inputmode="numeric"
min="0"
step="1"
class="input"
/>
<span class="mt-1 block text-xs text-muted">
Hide videos entirely once trusted contacts report spam at or above this count.
</span>
</label>
</div>
<div class="flex flex-wrap items-center gap-3">
<button
id="profileModerationSave"
type="button"
class="btn focus-ring"
disabled
>
Save moderation settings
</button>
<button
id="profileModerationReset"
type="button"
class="btn-ghost focus-ring"
>
Restore defaults
</button>
</div>
<p
id="profileModerationStatus"
class="text-sm text-muted"
role="status"
aria-live="polite"
></p>
</div>
</section>
<section
@@ -218,38 +352,38 @@
aria-labelledby="profileNavRelays"
>
<div class="space-y-4">
<p class="text-sm text-gray-400">
Configure the relays Bitvid will use to discover and publish your videos.
<p class="text-sm text-muted">
Configure the relays bitvid will use to discover and publish your videos.
</p>
<div class="flex flex-col gap-3 sm:flex-row sm:items-end">
<label class="flex-1 text-sm text-gray-300" for="relayInput">
<label class="flex-1 text-sm text-muted-strong" for="relayInput">
<span class="mb-2 block font-medium">Add a relay</span>
<input
id="relayInput"
type="url"
placeholder="wss://relay.example.com"
class="w-full rounded-md border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-100 placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
class="input"
/>
</label>
<div class="flex gap-2">
<button
id="addRelayBtn"
type="button"
class="px-4 py-2 rounded-md bg-blue-600 text-sm font-medium text-white hover:bg-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2 focus:ring-offset-gray-900"
class="btn focus-ring"
>
Add relay
</button>
<button
id="restoreRelaysBtn"
type="button"
class="px-4 py-2 rounded-md bg-gray-800 text-sm font-medium text-gray-200 hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2 focus:ring-offset-gray-900"
class="btn-ghost focus-ring"
>
Restore defaults
</button>
</div>
</div>
</div>
<ul id="relayList" class="space-y-3"></ul>
<ul id="relayList" class="grid-stack" data-orientation="vertical"></ul>
</section>
<section
@@ -259,20 +393,20 @@
aria-labelledby="profileNavWallet"
>
<div class="space-y-4">
<p class="text-sm text-gray-400">
Connect a Nostr Wallet Connect (NWC) compatible wallet to zap creators directly from BitVid.
<p class="text-sm text-muted">
Connect a Nostr Wallet Connect (NWC) compatible wallet to zap creators directly from bitvid.
</p>
<div class="space-y-4">
<label class="block text-sm text-gray-300" for="profileWalletUri">
<label class="block text-sm text-muted-strong" for="profileWalletUri">
<span class="mb-2 block font-medium">Wallet Connect URI</span>
<input
id="profileWalletUri"
type="url"
placeholder="nostr+walletconnect://<public-key>?relay=wss://relay.example.com"
class="w-full rounded-md border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-100 placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
class="input"
/>
</label>
<label class="block text-sm text-gray-300" for="profileWalletDefaultZap">
<label class="block text-sm text-muted-strong" for="profileWalletDefaultZap">
<span class="mb-2 block font-medium">Default zap amount (sats)</span>
<input
id="profileWalletDefaultZap"
@@ -281,7 +415,7 @@
min="0"
step="1"
placeholder="500"
class="w-full rounded-md border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-100 placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
class="input"
/>
</label>
</div>
@@ -289,28 +423,29 @@
<button
id="profileWalletSave"
type="button"
class="inline-flex items-center justify-center rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2 focus:ring-offset-gray-900"
class="btn focus-ring"
>
Save wallet
</button>
<button
id="profileWalletTest"
type="button"
class="inline-flex items-center justify-center rounded-md bg-gray-800 px-4 py-2 text-sm font-medium text-gray-200 transition hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2 focus:ring-offset-gray-900"
class="btn-ghost focus-ring"
>
Test connection
</button>
<button
id="profileWalletDisconnect"
type="button"
class="inline-flex items-center justify-center rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-red-500 focus:outline-none focus:ring-2 focus:ring-red-400 focus:ring-offset-2 focus:ring-offset-gray-900"
class="btn focus-ring"
data-variant="danger"
>
Disconnect wallet
</button>
</div>
<p
id="profileWalletStatus"
class="text-sm text-gray-400"
class="text-sm text-muted"
role="status"
aria-live="polite"
></p>
@@ -323,37 +458,37 @@
role="tabpanel"
aria-labelledby="profileNavBlocked"
>
<p class="text-sm text-gray-400">
Creators you block are hidden from your BitVid experience. This
<p class="text-sm text-muted">
Creators you block are hidden from your bitvid experience. This
list is private to your account.
</p>
<div class="flex flex-col gap-3 sm:flex-row sm:items-end">
<label class="flex-1 text-sm text-gray-300" for="blockedInput">
<label class="flex-1 text-sm text-muted-strong" for="blockedInput">
<span class="mb-2 block font-medium">Creator npub</span>
<input
id="blockedInput"
type="text"
inputmode="text"
autocomplete="off"
class="w-full rounded-md border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-100 placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
class="input"
placeholder="npub1..."
/>
</label>
<button
id="addBlockedBtn"
type="button"
class="px-4 py-2 rounded-md bg-blue-600 text-sm font-medium text-white hover:bg-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2 focus:ring-offset-gray-900"
class="btn focus-ring"
>
Block creator
</button>
</div>
<div
id="blockedEmpty"
class="rounded-lg border border-dashed border-gray-700 p-6 text-center text-sm text-gray-400"
class="card border border-dashed border-border/60 p-6 text-center text-sm text-muted"
>
You haven’t blocked any creators yet.
</div>
<ul id="blockedList" class="space-y-3"></ul>
<ul id="blockedList" class="grid-stack" data-orientation="vertical"></ul>
</section>
<section
id="profilePaneHistory"
@@ -361,12 +496,12 @@
role="tabpanel"
aria-labelledby="profileNavHistory"
>
<p class="text-sm text-gray-400">
<p class="text-sm text-muted">
Your recently watched videos appear here so you can jump back into favorites.
</p>
<div
id="profileHistoryFeatureBanner"
class="hidden rounded-md border border-amber-500/40 bg-amber-500/10 px-4 py-3 text-sm text-amber-100"
class="card hidden border border-status-warning-border bg-status-warning-surface px-4 py-3 text-sm text-status-warning-on"
role="status"
aria-live="polite"
></div>
@@ -380,14 +515,16 @@
<button
id="profileHistoryClear"
type="button"
class="inline-flex items-center gap-2 rounded-md bg-gray-800 px-4 py-2 text-sm font-medium text-gray-200 transition hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-900"
class="btn-ghost focus-ring inline-flex items-center gap-2"
data-size="sm"
>
Clear local cache
</button>
<button
id="profileHistoryRepublish"
type="button"
class="inline-flex items-center gap-2 rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2 focus:ring-offset-gray-900"
class="btn focus-ring inline-flex items-center gap-2"
data-size="sm"
>
Republish now
</button>
@@ -396,21 +533,21 @@
<div class="flex flex-col">
<span
id="profileHistoryMetadataLabel"
class="text-sm font-medium text-gray-200"
class="text-sm font-medium text-text"
>
Store titles/thumbnails locally
</span>
<span
id="profileHistoryMetadataDescription"
class="text-xs text-gray-400"
class="text-xs text-muted"
>
When disabled, BitVid skips saving rich metadata to this device.
When disabled, bitvid skips saving rich metadata to this device.
</span>
</div>
<button
id="profileHistoryMetadataToggle"
type="button"
class="relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer items-center rounded-full border border-gray-600 bg-gray-700 transition focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-900"
class="focus-ring relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer items-center rounded-full border border-border bg-panel transition"
role="switch"
aria-checked="true"
aria-labelledby="profileHistoryMetadataLabel"
@@ -433,7 +570,8 @@
></div>
<div
id="profileHistorySessionWarning"
class="hidden inline-flex items-center gap-2 rounded-full border border-amber-500/50 bg-amber-500/10 px-3 py-1 text-xs font-medium text-amber-100"
class="hidden pill"
data-variant="warning"
role="status"
aria-live="polite"
>
@@ -460,20 +598,22 @@
</div>
<div
id="profileHistoryError"
class="hidden rounded-md border border-red-500/40 bg-red-500/10 px-4 py-3 text-sm text-red-100"
class="card hidden border border-critical/40 bg-critical/10 px-4 py-3 text-sm text-critical"
role="alert"
aria-live="assertive"
></div>
<div
id="profileHistoryEmpty"
class="hidden rounded-lg border border-dashed border-gray-700 p-6 text-center text-sm text-gray-400"
class="card hidden border border-dashed border-border/60 p-6 text-center text-sm text-muted"
>
You haven’t watched any videos yet.
</div>
<div id="profileHistoryScroll" class="relative max-h-[420px] overflow-y-auto pr-1">
<div id="profileHistoryScroll" class="relative max-h-profile-history overflow-y-auto pr-1">
<div
id="profileHistoryGrid"
class="watch-history-grid watch-history-grid--modal hidden"
class="grid-stack hidden"
data-variant="cards"
data-orientation="vertical"
role="list"
></div>
<div id="profileHistorySentinel" class="h-1 w-full"></div>
@@ -487,121 +627,121 @@
aria-labelledby="profileNavAdmin"
>
<div class="space-y-2">
<h3 class="text-lg font-semibold text-white">Platform moderation</h3>
<p class="text-sm text-gray-400">
Manage BitVid’s moderator roles and creator allow/block lists. Changes apply instantly across the app.
<h3 class="text-base font-semibold text-text">Platform moderation</h3>
<p class="text-sm text-muted">
Manage bitvid’s moderator roles and creator allow/block lists. Changes apply instantly across the app.
</p>
</div>
<div id="adminModeratorsSection" class="space-y-4 hidden" aria-hidden="true">
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<h4 class="text-base font-semibold text-white">Moderators</h4>
<p class="text-xs uppercase tracking-wide text-gray-400">
<h4 class="text-base font-semibold text-text">Moderators</h4>
<p class="text-xs uppercase tracking-wide text-muted">
Super Admin only
</p>
</div>
<p class="text-sm text-gray-400">
<p class="text-sm text-muted">
Moderators can edit the whitelist and blacklist but cannot manage other moderators or whitelist mode.
</p>
<div class="flex flex-col gap-3 sm:flex-row sm:items-end">
<label class="flex-1 text-sm text-gray-300" for="adminModeratorInput">
<label class="flex-1 text-sm text-muted-strong" for="adminModeratorInput">
<span class="mb-2 block font-medium">Moderator npub</span>
<input
id="adminModeratorInput"
type="text"
inputmode="text"
autocomplete="off"
class="w-full rounded-md border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-100 placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
class="input"
placeholder="npub1..."
/>
</label>
<button
id="adminAddModeratorBtn"
type="button"
class="px-4 py-2 rounded-md bg-blue-600 text-sm font-medium text-white hover:bg-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2 focus:ring-offset-gray-900"
class="btn focus-ring"
>
Add moderator
</button>
</div>
<div
id="adminModeratorsEmpty"
class="rounded-lg border border-dashed border-gray-700 p-4 text-center text-sm text-gray-400"
class="card border border-dashed border-border/60 p-4 text-center text-sm text-muted"
>
No moderators configured yet.
</div>
<ul id="adminModeratorList" class="space-y-3"></ul>
<ul id="adminModeratorList" class="grid-stack" data-orientation="vertical"></ul>
</div>
<div id="adminWhitelistSection" class="space-y-4">
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<h4 class="text-base font-semibold text-white">Whitelist</h4>
<h4 class="text-base font-semibold text-text">Whitelist</h4>
</div>
<p class="text-sm text-gray-400">
<p class="text-sm text-muted">
Creators listed here remain visible when whitelist mode is enabled. Adding someone removes them from the blacklist.
</p>
<div class="flex flex-col gap-3 sm:flex-row sm:items-end">
<label class="flex-1 text-sm text-gray-300" for="adminWhitelistInput">
<label class="flex-1 text-sm text-muted-strong" for="adminWhitelistInput">
<span class="mb-2 block font-medium">Creator npub</span>
<input
id="adminWhitelistInput"
type="text"
inputmode="text"
autocomplete="off"
class="w-full rounded-md border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-100 placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
class="input"
placeholder="npub1..."
/>
</label>
<button
id="adminAddWhitelistBtn"
type="button"
class="px-4 py-2 rounded-md bg-blue-600 text-sm font-medium text-white hover:bg-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2 focus:ring-offset-gray-900"
class="btn focus-ring"
>
Add to whitelist
</button>
</div>
<div
id="adminWhitelistEmpty"
class="rounded-lg border border-dashed border-gray-700 p-4 text-center text-sm text-gray-400"
class="card border border-dashed border-border/60 p-4 text-center text-sm text-muted"
>
No whitelisted creators yet.
</div>
<ul id="adminWhitelistList" class="space-y-3"></ul>
<ul id="adminWhitelistList" class="grid-stack" data-orientation="vertical"></ul>
</div>
<div id="adminBlacklistSection" class="space-y-4">
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<h4 class="text-base font-semibold text-white">Blacklist</h4>
<h4 class="text-base font-semibold text-text">Blacklist</h4>
</div>
<p class="text-sm text-gray-400">
Creators on the blacklist are hidden from BitVid everywhere. Adding someone removes them from the whitelist.
<p class="text-sm text-muted">
Creators on the blacklist are hidden from bitvid everywhere. Adding someone removes them from the whitelist.
</p>
<div class="flex flex-col gap-3 sm:flex-row sm:items-end">
<label class="flex-1 text-sm text-gray-300" for="adminBlacklistInput">
<label class="flex-1 text-sm text-muted-strong" for="adminBlacklistInput">
<span class="mb-2 block font-medium">Creator npub</span>
<input
id="adminBlacklistInput"
type="text"
inputmode="text"
autocomplete="off"
class="w-full rounded-md border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-100 placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
class="input"
placeholder="npub1..."
/>
</label>
<button
id="adminAddBlacklistBtn"
type="button"
class="px-4 py-2 rounded-md bg-blue-600 text-sm font-medium text-white hover:bg-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2 focus:ring-offset-gray-900"
class="btn focus-ring"
>
Add to blacklist
</button>
</div>
<div
id="adminBlacklistEmpty"
class="rounded-lg border border-dashed border-gray-700 p-4 text-center text-sm text-gray-400"
class="card border border-dashed border-border/60 p-4 text-center text-sm text-muted"
>
No blacklisted creators yet.
</div>
<ul id="adminBlacklistList" class="space-y-3"></ul>
<ul id="adminBlacklistList" class="grid-stack" data-orientation="vertical"></ul>
</div>
</section>
</div>
@@ -609,11 +749,8 @@
</div>
<!-- Modal Footer -->
<div class="p-4 border-t border-gray-700 flex items-center justify-end">
<button
id="profileLogoutBtn"
class="bg-red-500 text-white px-4 py-2 rounded-md hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 focus:ring-offset-gray-900"
>
<div class="flex items-center justify-end gap-3 border-t border-border/60 p-4">
<button id="profileLogoutBtn" class="btn focus-ring" data-variant="danger">
Logout
</button>
</div>
+100 -68
View File
@@ -1,44 +1,38 @@
<!-- components/revert-video-modal.html -->
<div
id="revertVideoModal"
class="fixed inset-0 z-50 hidden"
style="background: transparent"
class="bv-modal hidden items-start justify-center md:items-center"
role="dialog"
aria-modal="true"
aria-labelledby="revertModalTitle"
aria-describedby="revertModalSubtitle"
data-ds="new"
>
<div
id="revertVideoModalOverlay"
class="absolute inset-0 z-10"
style="
background-color: rgba(0, 0, 0, 0.9);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
"
class="bv-modal-backdrop"
data-dismiss
></div>
<div
class="relative modal-container h-full w-full flex items-start md:items-center justify-center overflow-y-auto z-20"
>
<div
class="modal-content bg-gray-900 w-full max-w-[95%] lg:max-w-5xl my-0 rounded-lg overflow-hidden relative"
style="max-height: 92vh"
>
<div
class="sticky top-0 bg-gradient-to-b from-black/80 to-transparent transition-transform duration-150 p-4 flex items-center justify-between"
>
<div class="modal-sheet max-h-modal-sheet overflow-hidden" tabindex="-1">
<div class="modal-header transition-transform duration-150">
<div class="flex flex-col">
<h2 id="revertModalTitle" class="text-xl font-bold text-white">
<h2 id="revertModalTitle" class="text-xl font-bold text-text">
Revert Video Note
</h2>
<p id="revertModalSubtitle" class="text-xs text-gray-400">
<p id="revertModalSubtitle" class="text-xs text-muted">
Review previous versions before restoring an older state.
</p>
</div>
<button
id="closeRevertVideoModal"
class="flex items-center justify-center w-10 h-10 rounded-full bg-black/50 hover:bg-black/70 transition-all duration-200 backdrop-blur focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-black focus:ring-blue-500"
class="modal-close"
type="button"
aria-label="Close revert modal"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-gray-300"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
@@ -53,119 +47,157 @@
</button>
</div>
<div class="p-6 space-y-6 overflow-y-auto" style="max-height: calc(92vh - 4rem)">
<div class="rounded-md border border-gray-700 bg-gray-800/60 p-4 text-sm text-gray-300">
<div class="modal-body space-y-6 overflow-y-auto max-h-modal-body">
<div
class="rounded-md border border-border/60 bg-panel/60 p-4 text-sm text-muted-strong"
>
<p>
Selecting an older version will mark any newer edits as reverted.
The previous revision will become the active note on the relays.
</p>
<p class="mt-2 text-xs text-gray-400">
Versions are grouped by their <code class="px-1 py-0.5 bg-gray-900/60 rounded border border-gray-700 text-[0.7rem]">d</code>
tag or shared <code class="px-1 py-0.5 bg-gray-900/60 rounded border border-gray-700 text-[0.7rem]">videoRootId</code>. This ensures legacy
clients see consistent history when multiple edits were published.
<p class="mt-2 text-xs text-muted">
Versions are grouped by their
<code
class="rounded border border-border/60 bg-surface/60 px-1 py-0.5 text-2xs"
>d</code
>
tag or shared
<code
class="rounded border border-border/60 bg-surface/60 px-1 py-0.5 text-2xs"
>videoRootId</code
>. This ensures legacy clients see consistent history when multiple
edits were published, and keeps audience flags (NSFW / for kids)
aligned with the Upload modal.
</p>
</div>
<div class="grid gap-6 lg:grid-cols-[minmax(240px,280px)_1fr]">
<div class="revert-modal__layout">
<aside class="space-y-4">
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold text-gray-200">Revision history</h3>
<h3 class="text-sm font-semibold text-text">
Revision history
</h3>
<span
id="revertHistoryCount"
class="rounded-full bg-gray-800 px-2 py-0.5 text-xs text-gray-400"
class="rounded-full bg-panel px-2 py-0.5 text-xs text-muted"
></span>
</div>
<div
id="revertVersionsList"
class="space-y-2 overflow-y-auto pr-1"
style="max-height: 48vh"
class="space-y-2 overflow-y-auto pr-1 max-h-modal-pane"
></div>
</aside>
<section
id="revertVersionDetails"
class="rounded-lg border border-gray-800 bg-gray-900/70 p-4 text-sm text-gray-200"
class="rounded-lg border border-border/70 bg-surface/70 p-4 text-sm text-text"
aria-live="polite"
>
<!-- NOTE: Keep Upload/Edit metadata fields mirrored when adjusting these sections. -->
<div class="space-y-6" aria-labelledby="revertVersionPlaceholderHeading">
<div
class="space-y-6"
aria-labelledby="revertVersionPlaceholderHeading"
>
<div class="space-y-2 text-center">
<h3 id="revertVersionPlaceholderHeading" class="text-sm font-semibold text-gray-200">
<h3
id="revertVersionPlaceholderHeading"
class="text-sm font-semibold text-text"
>
Revision metadata
</h3>
<p
id="revertVersionPlaceholder"
class="text-sm text-gray-500"
>
<p id="revertVersionPlaceholder" class="text-sm text-muted">
Select an earlier revision to inspect its metadata.
</p>
</div>
<section class="space-y-2">
<h4 class="text-xs font-semibold uppercase tracking-wide text-gray-400">
<h4
class="text-xs font-semibold uppercase tracking-wide text-muted"
>
Playback sources
</h4>
<dl class="grid gap-2 text-xs text-gray-400 sm:grid-cols-2">
<div>
<dt class="font-semibold text-gray-300">Hosted URL</dt>
<dd class="mt-1 text-gray-500">Awaiting selection</dd>
<dl class="grid gap-2 text-xs text-muted sm:grid-cols-2">
<div class="space-y-1">
<dt class="font-semibold text-muted-strong">Hosted URL</dt>
<dd class="text-muted">Awaiting selection</dd>
</div>
<div>
<dt class="font-semibold text-gray-300">Magnet</dt>
<dd class="mt-1 text-gray-500">Awaiting selection</dd>
<div class="space-y-1">
<dt class="font-semibold text-muted-strong">Magnet</dt>
<dd class="text-muted">Awaiting selection</dd>
</div>
</dl>
</section>
<section class="space-y-2">
<h4 class="text-xs font-semibold uppercase tracking-wide text-gray-400">
<h4
class="text-xs font-semibold uppercase tracking-wide text-muted"
>
Audience flags
</h4>
<dl class="grid gap-2 text-xs text-muted sm:grid-cols-2">
<div class="space-y-1">
<dt class="font-semibold text-muted-strong">NSFW flag</dt>
<dd class="text-muted">Awaiting selection</dd>
</div>
<div class="space-y-1">
<dt class="font-semibold text-muted-strong">For kids flag</dt>
<dd class="text-muted">Awaiting selection</dd>
</div>
</dl>
</section>
<section class="space-y-2">
<h4
class="text-xs font-semibold uppercase tracking-wide text-muted"
>
NIP-71 event metadata
</h4>
<dl class="grid gap-2 text-xs text-gray-400 sm:grid-cols-2">
<div>
<dt class="font-semibold text-gray-300">Kind</dt>
<dd class="mt-1 text-gray-500">Awaiting selection</dd>
<dl class="grid gap-2 text-xs text-muted sm:grid-cols-2">
<div class="space-y-1">
<dt class="font-semibold text-muted-strong">Kind</dt>
<dd class="text-muted">Awaiting selection</dd>
</div>
<div>
<dt class="font-semibold text-gray-300">Duration</dt>
<dd class="mt-1 text-gray-500">Awaiting selection</dd>
<div class="space-y-1">
<dt class="font-semibold text-muted-strong">Duration</dt>
<dd class="text-muted">Awaiting selection</dd>
</div>
<div class="sm:col-span-2">
<dt class="font-semibold text-gray-300">Summary</dt>
<dd class="mt-1 text-gray-500">Awaiting selection</dd>
<div class="space-y-1 sm:col-span-2">
<dt class="font-semibold text-muted-strong">Summary</dt>
<dd class="text-muted">Awaiting selection</dd>
</div>
</dl>
</section>
<section class="space-y-2">
<h4 class="text-xs font-semibold uppercase tracking-wide text-gray-400">
<h4
class="text-xs font-semibold uppercase tracking-wide text-muted"
>
NIP-71 media metadata
</h4>
<p class="text-xs text-gray-500">
Media variants, text tracks, chapters, hashtags, participants, and references will appear once a revision is selected.
<p class="text-xs text-muted">
Media variants, text tracks, chapters, hashtags, participants,
and references will appear once a revision is selected.
</p>
</section>
</div>
</section>
</div>
<p
id="revertSelectionStatus"
class="text-xs text-gray-400"
></p>
<p id="revertSelectionStatus" class="text-xs text-muted"></p>
<div class="flex flex-col gap-3 sm:flex-row sm:justify-end">
<button
type="button"
id="cancelRevertVideo"
class="w-full sm:w-auto px-4 py-2 rounded-md bg-gray-800 text-gray-200 hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-gray-500"
class="btn-ghost w-full sm:w-auto"
>
Cancel
</button>
<button
type="button"
id="confirmRevertVideo"
class="w-full sm:w-auto px-4 py-2 rounded-md bg-red-600 text-white hover:bg-red-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-red-500 disabled:opacity-60 disabled:cursor-not-allowed"
class="btn w-full sm:w-auto"
data-variant="critical"
disabled
>
Revert to selected version
+42 -12
View File
@@ -7,6 +7,7 @@
id="sidebar"
class="fade-in fade-in-delay-100 sidebar-collapsed"
data-state="collapsed"
data-ds="new"
>
<div class="sidebar__inner">
<div class="sidebar-scroll">
@@ -167,7 +168,13 @@
/>
<span class="sidebar-text">Roadmap</span>
</a>
<a href="#view=blog" class="sidebar-nav-link sidebar-nav-link--sub" title="Blog">
<a
href="#"
data-blog-link
class="sidebar-nav-link sidebar-nav-link--sub"
title="Blog"
rel="noopener noreferrer"
>
<img
src="assets/svg/blog-icon.svg"
alt=""
@@ -177,11 +184,11 @@
<span class="sidebar-text">Blog</span>
</a>
<a
href="https://primal.net/p/npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe"
target="_blank"
rel="noopener noreferrer"
href="#"
data-nostr-link
class="sidebar-nav-link sidebar-nav-link--sub"
title="Nostr"
rel="noopener noreferrer"
>
<img
src="assets/svg/nostr-icon.svg"
@@ -192,11 +199,34 @@
<span class="sidebar-text">Nostr</span>
</a>
<a
href="https://github.com/PR0M3TH3AN/bitvid"
target="_blank"
href="#"
data-community-link
class="sidebar-nav-link sidebar-nav-link--sub"
title="Community"
rel="noopener noreferrer"
>
<svg
class="sidebar-icon"
width="24"
height="24"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
fill-rule="evenodd"
clip-rule="evenodd"
aria-hidden="true"
>
<path
d="M24 20h-3v4l-5.333-4h-7.667v-4h2v2h6.333l2.667 2v-2h3v-8.001h-2v-2h4v12.001zm-6-6h-9.667l-5.333 4v-4h-3v-14.001h18v14.001zm-13-8c.552 0 1 .448 1 1s-.448 1-1 1-1-.448-1-1 .448-1 1-1zm4 0c.552 0 1 .448 1 1s-.448 1-1 1-1-.448-1-1 .448-1 1-1zm4 0c.552 0 1 .448 1 1s-.448 1-1 1-1-.448-1-1 .448-1 1-1z"
/>
</svg>
<span class="sidebar-text">Community</span>
</a>
<a
href="#"
data-github-link
class="sidebar-nav-link sidebar-nav-link--sub"
title="GitHub"
rel="noopener noreferrer"
>
<img
src="assets/svg/github-icon.svg"
@@ -222,11 +252,11 @@
<span class="sidebar-text">βeacon</span>
</a>
<a
href="https://beta.bitvid.network/"
target="_blank"
rel="noopener noreferrer"
href="#"
data-beta-link
class="sidebar-nav-link sidebar-nav-link--sub"
title="Beta"
rel="noopener noreferrer"
>
<img
src="assets/svg/beta-icon.svg"
@@ -255,11 +285,11 @@
<span class="sidebar-text">IPNS</span>
</a>
<a
href="https://bitvid.network/"
target="_blank"
rel="noopener noreferrer"
href="#"
data-dns-link
class="sidebar-nav-link sidebar-nav-link--sub"
title="DNS"
rel="noopener noreferrer"
>
<img
src="assets/svg/dns-icon.svg"
+1083 -470
View File
File diff suppressed because it is too large. Load diff
+313 -303
View File
@@ -1,323 +1,333 @@
<!-- components/video-modal.html -->
<div id="playerModal" class="player-modal hidden">
<div class="player-modal__container">
<div class="player-modal__content">
<!-- Navigation bar - sliding at top -->
<div
id="modalNav"
class="modal-nav sticky top-0 z-60 transition-transform duration-150"
>
<div class="flex items-center px-6 py-4">
<button
id="closeModal"
class="back-button flex items-center justify-center w-10 h-10 rounded-full bg-black/50 hover:bg-black/70 transition-all duration-200 backdrop-blur focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-black focus:ring-blue-500"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="w-6 h-6 text-gray-300"
>
<path d="M15 18l-6-6 6-6" />
</svg>
</button>
<div
id="playerModal"
class="bv-modal hidden"
role="dialog"
aria-modal="true"
data-ds="new"
hidden
>
<div class="bv-modal-backdrop" data-dismiss></div>
<div class="bv-modal__panel" tabindex="-1">
<div class="player-modal__container video-modal__container">
<div id="modalNav" class="modal-nav modal-nav--visible">
<div class="watch-container modal-nav__bar">
<div class="modal-actions">
<button id="copyMagnetBtn" class="icon-button">
<img
src="assets/svg/copy-magnet.svg"
alt="Copy Magnet"
class="icon-image"
/>
</button>
<div class="relative" data-modal-zap-wrapper>
<button
id="modalZapBtn"
type="button"
class="icon-button"
aria-label="Send a zap"
aria-haspopup="dialog"
aria-expanded="false"
aria-controls="modalZapDialog"
>
<img
src="assets/svg/lightning-bolt.svg"
alt="Zap"
class="icon-image"
/>
</button>
<div
id="modalZapDialog"
class="zap-popover hidden"
role="dialog"
<button
id="closeModal"
type="button"
class="icon-button focus-ring accent-action-button"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-5 w-5"
aria-hidden="true"
>
<div
class="zap-popover__content w-72 max-w-[calc(100vw-2rem)] rounded-lg p-4 shadow-lg"
>
<div class="mb-3 flex items-center justify-between gap-4">
<h3
class="text-xs font-semibold uppercase tracking-wide text-gray-300"
>
Send a zap
</h3>
<button
type="button"
id="modalZapCloseBtn"
class="text-gray-400 transition-colors hover:text-gray-200"
aria-label="Close zap dialog"
>
✕
</button>
</div>
<p
id="modalZapWalletPrompt"
class="mb-3 hidden text-sm text-gray-200"
aria-hidden="true"
>
Connect a wallet in
<button
type="button"
id="modalZapWalletLink"
class="font-semibold text-blue-300 underline decoration-dotted transition-colors hover:text-blue-200"
>
Wallet Connect settings
</button>
to send zaps.
</p>
<form id="modalZapForm" class="space-y-3">
<div>
<label
for="modalZapAmountInput"
class="mb-2 block text-xs font-semibold uppercase tracking-wide text-gray-300"
>
Zap amount (sats)
</label>
<input
id="modalZapAmountInput"
type="number"
min="1"
step="1"
inputmode="numeric"
class="w-full rounded border border-gray-600 bg-gray-800 px-3 py-2 text-sm text-gray-100 focus:border-blue-400 focus:outline-none focus:ring-1 focus:ring-blue-400"
placeholder="Enter sats"
/>
</div>
<div>
<label
for="modalZapCommentInput"
class="mb-2 block text-xs font-semibold uppercase tracking-wide text-gray-300"
>
Comment (optional)
</label>
<textarea
id="modalZapCommentInput"
rows="2"
maxlength="280"
class="w-full resize-y rounded border border-gray-600 bg-gray-800 px-3 py-2 text-sm text-gray-100 focus:border-blue-400 focus:outline-none focus:ring-1 focus:ring-blue-400"
placeholder="Include a note with your zap"
></textarea>
</div>
<p
id="modalZapSplitSummary"
class="text-sm text-gray-300"
aria-live="polite"
>
Enter an amount to view the split.
</p>
<p
id="modalZapStatus"
class="text-xs text-gray-400"
role="status"
aria-live="polite"
></p>
<ul
id="modalZapReceipts"
class="space-y-2 text-xs text-gray-300"
></ul>
<div class="flex items-center justify-end gap-2">
<button
type="submit"
id="modalZapSendBtn"
class="rounded bg-blue-500 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2 focus:ring-offset-gray-900"
>
Send
</button>
</div>
</form>
</div>
</div>
</div>
<button id="shareBtn" class="icon-button">
<img
src="assets/svg/share-video.svg"
alt="Share Video"
class="icon-image"
/>
<path d="M15 18l-6-6 6-6" />
</svg>
<span class="sr-only">Close player</span>
</button>
<div
class="modal-actions__overflow"
data-more-menu-wrapper="true"
>
<button
id="modalMoreBtn"
type="button"
class="icon-button"
aria-haspopup="true"
aria-expanded="false"
aria-label="More"
data-more-dropdown="modal"
>
<img src="assets/svg/ellipsis.svg" alt="More" class="w-5 h-5" />
</button>
<div
id="moreDropdown-modal"
class="hidden absolute right-0 top-14 w-44 rounded-md shadow-lg bg-gray-800 ring-1 ring-black ring-opacity-5 z-50"
role="menu"
data-more-menu="true"
>
<div class="py-1">
<button
class="block w-full text-left px-4 py-2 text-sm text-gray-100 hover:bg-gray-700"
data-action="open-channel"
data-context="modal"
>
Open channel
</button>
<button
class="block w-full text-left px-4 py-2 text-sm text-gray-100 hover:bg-gray-700"
data-action="copy-link"
data-context="modal"
>
Copy link
</button>
</div>
</div>
</div>
<div class="player-modal__content video-modal__panel flex flex-col gap-6">
<div class="video-modal__layout">
<div class="video-modal__primary flex flex-col gap-6">
<div class="watch-container">
<div class="video-modal__video">
<video
id="modalVideo"
class="video-modal__media"
controls
></video>
</div>
</div>
<div class="watch-container">
<div class="modal-info-panel">
<div class="video-modal__details flex flex-col gap-6">
<div
class="video-modal__headline flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between"
>
<h2 id="videoTitle" class="video-modal__title"></h2>
<div
class="px-4 pt-2 pb-1 text-xs font-semibold uppercase tracking-wide text-gray-400"
id="modalStatus"
class="video-modal__status text-sm text-muted lg:text-right"
role="status"
aria-live="polite"
>
Boost on Nostr…
Initializing... Just give it a sec.
</div>
</div>
<div
class="video-modal__meta flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"
>
<div class="video-modal__meta-info flex flex-wrap items-center gap-3">
<span id="videoTimestamp" class="modal-meta">Posted just now</span>
<span id="videoEditedTimestamp" class="modal-meta hidden">Last edited just now</span>
<span id="videoViewCount" class="modal-meta modal-view-count">– views</span>
</div>
</div>
<div class="video-modal__actions-row flex flex-wrap items-center gap-3">
<button
id="copyMagnetBtn"
type="button"
class="video-modal__action-button focus-ring accent-action-button"
aria-label="Copy magnet"
>
<svg
class="video-modal__action-icon icon-image"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M17.374 20.235c2.444-2.981 6.626-8.157 6.626-8.157l-3.846-3.092s-2.857 3.523-6.571 8.097c-4.312 5.312-11.881-2.41-6.671-6.671 4.561-3.729 8.097-6.57 8.097-6.57l-3.092-3.842s-5.173 4.181-8.157 6.621c-2.662 2.175-3.76 4.749-3.76 7.24 0 5.254 4.867 10.139 10.121 10.139 2.487 0 5.064-1.095 7.253-3.765zm4.724-7.953l-1.699 2.111-1.74-1.397 1.701-2.114 1.738 1.4zm-10.386-10.385l1.4 1.738-2.113 1.701-1.397-1.74 2.11-1.699z"
/>
</svg>
<span class="video-modal__action-label">Copy magnet</span>
</button>
<div class="popover" data-modal-zap-wrapper>
<button
id="modalZapBtn"
type="button"
class="video-modal__action-button focus-ring accent-action-button"
aria-label="Send a zap"
aria-haspopup="dialog"
aria-expanded="false"
aria-controls="modalZapDialog"
>
<svg
class="video-modal__action-icon icon-image"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<polygon
points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<span class="video-modal__action-label">Zap</span>
</button>
<div
id="modalZapDialog"
class="popover__panel card w-72 max-w-popover-safe px-5 py-4"
data-variant="zap"
role="dialog"
aria-hidden="true"
data-state="closed"
hidden
>
<div class="grid gap-4">
<div class="mb-4 flex items-center justify-between gap-4">
<h3 class="text-xs font-semibold uppercase tracking-wide text-muted">
Send a zap
</h3>
<button
type="button"
id="modalZapCloseBtn"
class="focus-ring inline-flex h-8 w-8 items-center justify-center rounded-full text-muted transition hover:text-text"
aria-label="Close zap dialog"
>
✕
</button>
</div>
<p
id="modalZapWalletPrompt"
class="mb-3 text-sm text-muted"
aria-hidden="true"
hidden
>
Connect a wallet in
<button
type="button"
id="modalZapWalletLink"
class="focus-ring inline-flex items-center gap-1 text-sm font-semibold text-info underline decoration-dotted underline-offset-2 hover:text-info-strong"
>
Wallet Connect settings
</button>
to send zaps.
</p>
<form id="modalZapForm" class="space-y-4">
<div class="space-y-2">
<label
for="modalZapAmountInput"
class="text-xs font-semibold uppercase tracking-wide text-muted"
>
Zap amount (sats)
</label>
<input
id="modalZapAmountInput"
type="number"
min="1"
step="1"
inputmode="numeric"
class="input"
placeholder="Enter sats"
/>
</div>
<div class="space-y-2">
<label
for="modalZapCommentInput"
class="text-xs font-semibold uppercase tracking-wide text-muted"
>
Comment (optional)
</label>
<textarea
id="modalZapCommentInput"
rows="2"
maxlength="280"
class="input resize-y"
placeholder="Include a note with your zap"
></textarea>
</div>
<p
id="modalZapSplitSummary"
class="text-sm text-muted"
aria-live="polite"
>
Enter an amount to view the split.
</p>
<p
id="modalZapStatus"
class="text-xs text-muted"
role="status"
aria-live="polite"
></p>
<ul
id="modalZapReceipts"
class="space-y-2 text-xs text-muted"
></ul>
<div class="flex items-center justify-end gap-2">
<button
type="submit"
id="modalZapSendBtn"
class="btn"
>
Send
</button>
</div>
</form>
</div>
</div>
</div>
<button
class="block w-full text-left px-4 py-2 text-sm text-gray-100 hover:bg-gray-700"
data-action="repost-event"
data-context="modal"
id="shareBtn"
type="button"
class="video-modal__action-button focus-ring accent-action-button"
aria-label="Share video"
>
Repost (kind 6)
<svg
class="video-modal__action-icon icon-image"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M5 7a5 5 0 1 1 0 10 5 5 0 0 1 0-10Zm11.122 12.065A4 4 0 1 1 24 20a4 4 0 0 1-7.878-.935l-5.488-2.927a6.009 6.009 0 0 1-.943 1.764l5.488 2.927ZM24 4a4 4 0 1 1-7.878.935l-5.488 2.927a6.01 6.01 0 0 1-.943-1.764l5.488-2.927A4 4 0 0 1 24 4Z"
/>
</svg>
<span class="video-modal__action-label">Share</span>
</button>
<button
class="block w-full text-left px-4 py-2 text-sm text-gray-100 hover:bg-gray-700"
data-action="mirror-video"
data-context="modal"
<div class="relative" data-more-menu-wrapper="true">
<button
id="modalMoreBtn"
type="button"
class="video-modal__action-button focus-ring accent-action-button"
aria-haspopup="true"
aria-expanded="false"
aria-label="More actions"
data-more-dropdown="modal"
>
<svg
class="video-modal__action-icon icon-image"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<circle cx="5" cy="12" r="2" fill="currentColor" />
<circle cx="12" cy="12" r="2" fill="currentColor" />
<circle cx="19" cy="12" r="2" fill="currentColor" />
</svg>
<span class="video-modal__action-label">More</span>
</button>
</div>
</div>
<div class="channel-card">
<div class="channel-card__avatar">
<img
id="creatorAvatar"
src=""
alt="Creator"
class="h-full w-full rounded-full object-cover cursor-pointer"
/>
</div>
<div class="flex flex-col gap-1">
<h3 id="creatorName" class="channel-card__name cursor-pointer">
Creator Name
</h3>
<p id="creatorNpub" class="channel-card__npub">npub...</p>
</div>
</div>
<div class="description-panel">
<p
id="videoDescription"
class="video-modal__description-body whitespace-pre-wrap"
>
Mirror (kind 1063)
</button>
<button
class="block w-full text-left px-4 py-2 text-sm text-gray-100 hover:bg-gray-700"
data-action="ensure-presence"
data-context="modal"
>
Rebroadcast
</button>
<div class="my-1 border-t border-gray-700 opacity-70"></div>
<button
class="block w-full text-left px-4 py-2 text-sm text-red-400 hover:bg-red-700 hover:text-white"
data-action="blacklist-author"
data-context="modal"
data-blacklist-menu="true"
>
Blacklist creator
</button>
<button
class="block w-full text-left px-4 py-2 text-sm text-red-400 hover:bg-red-700 hover:text-white"
data-action="block-author"
data-context="modal"
>
Block creator
</button>
<button
class="block w-full text-left px-4 py-2 text-sm text-gray-100 hover:bg-gray-700"
data-action="report"
data-context="modal"
>
Report
</button>
No description available.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="player-modal__video modal-video-shell">
<video id="modalVideo" class="w-full aspect-video" controls></video>
</div>
<div class="player-modal__info modal-info-panel">
<!-- Title and icons row -->
<div class="flex items-center justify-between mb-2">
<h2 id="videoTitle" class="text-2xl font-bold text-white"></h2>
</div>
<!-- Video Timestamp -->
<div
class="flex items-start justify-between text-sm text-gray-400 mb-4"
>
<div class="flex flex-wrap items-center gap-2 modal-meta">
<span id="videoTimestamp">Posted just now</span>
<span
id="videoEditedTimestamp"
class="hidden"
>Last edited just now</span>
<span id="videoViewCount" class="modal-view-count">– views</span>
</div>
<div id="modalStatus" class="text-gray-300">
Initializing... Just give it a sec.
</div>
</div>
<!-- Creator info -->
<div class="flex items-center mb-4 p-4 bg-gray-800/50 rounded-lg">
<div class="w-12 h-12 rounded-full bg-gray-700 overflow-hidden">
<img
id="creatorAvatar"
src=""
alt="Creator"
class="w-full h-full object-cover"
/>
</div>
<div class="ml-4">
<h3 id="creatorName" class="font-medium text-lg text-white">
Creator Name
</h3>
<p id="creatorNpub" class="text-sm text-gray-400">npub...</p>
</div>
</div>
<!-- Video Description -->
<div class="bg-gray-800/50 rounded-lg p-4 mb-4">
<p id="videoDescription" class="text-gray-300 whitespace-pre-wrap">
No description available.
</p>
</div>
<!-- Torrent stats -->
<div class="bg-gray-800/50 rounded-lg p-4">
<div class="w-full bg-gray-700 rounded-full h-2 mb-2">
<div
class="bg-blue-500 h-2 rounded-full"
id="modalProgress"
style="width: 0%"
></div>
</div>
<div class="flex justify-between text-sm text-gray-400">
<span id="modalPeers">Peers: 0</span>
<span id="modalSpeed">Speed: 0 KB/s</span>
<span id="modalDownloaded">Downloaded: 0 MB / 0 MB</span>
</div>
<aside class="video-modal__secondary flex flex-col gap-4">
<div class="watch-container">
<div class="video-modal__stats rounded-2xl border border-border border-opacity-20 bg-surface px-6 py-5 shadow-sm">
<div class="video-modal__progress">
<progress
id="modalProgress"
class="progress"
max="100"
value="0"
data-variant="accent"
aria-label="Download progress"
></progress>
<p
id="modalProgressStatus"
class="sr-only"
role="status"
aria-live="polite"
></p>
</div>
<div class="video-modal__stat-metrics flex flex-wrap items-center justify-between gap-3 text-sm text-muted">
<span id="modalPeers">Peers: 0</span>
<span id="modalSpeed">Speed: 0 KB/s</span>
<span id="modalDownloaded">Downloaded: 0 MB / 0 MB</span>
</div>
</div>
</div>
</aside>
</div>
</div>
</div>
+1 -1
View File
@@ -1 +1 @@
export const ASSET_VERSION = "2024.10.29";
export const ASSET_VERSION = "2024.11.09";
+124 -17
View File
@@ -1,22 +1,42 @@
// config/instance-config.js
// -----------------------------------------------------------------------------
// BitVid instance configuration
// bitvid instance configuration
// -----------------------------------------------------------------------------
//
// This file consolidates the values that operators usually customize when they
// deploy their own BitVid instance. Update the exports below to match your
// deploy their own bitvid instance. Update the exports below to match your
// environment, commit the changes, and redeploy. Leaving this file in the repo
// (instead of hidden in environment variables) makes it easy for future
// maintainers to understand how your instance is configured.
//
// Each setting includes guidance about how BitVid uses the value and what
// Each setting includes guidance about how bitvid uses the value and what
// adjustments are safe. When in doubt, mirror the structure shown here so other
// contributors can follow along.
/**
* The primary administrator for this BitVid instance.
* Whether the current deployment should enable developer-centric behavior.
*
* BitVid treats this Nostr public key (npub) as the "Super Admin". This user
* When `true`, bitvid surfaces extra logging, debug helpers, and experimental
* UI affordances intended for development environments. Set the value to
* `false` before deploying to production so that end users receive the stable
* experience. Downstream modules read this flag via `js/config.js`.
*/
export const IS_DEV_MODE = true;
/**
* Whether the current deployment should operate in lockdown mode.
*
* When `true`, bitvid restricts access to operator-approved functionality and
* enables defensive UI copy intended for incidents or maintenance windows.
* Leave this `false` for normal operation so downstream modules (via
* `js/config.js`) continue exposing the full experience.
*/
export const IS_LOCKDOWN_MODE = false;
/**
* The primary administrator for this bitvid instance.
*
* bitvid treats this Nostr public key (npub) as the "Super Admin". This user
* cannot be removed from moderator lists, and only they can promote new
* moderators or toggle whitelist-only mode. Replace the string below with the
* npub of the account you want to act as the ultimate authority for your
@@ -26,7 +46,7 @@ export const ADMIN_SUPER_NPUB =
"npub15jnttpymeytm80hatjqcvhhqhzrhx6gxp8pq0wn93rhnu8s9h9dsha32lx";
/**
* Canonical URL for the public BitVid site.
* Canonical URL for the public bitvid site.
*
* Surfaces in admin outreach copy and moderation DMs so that recipients can
* quickly jump back to the primary destination. Update this if your
@@ -34,10 +54,66 @@ export const ADMIN_SUPER_NPUB =
*/
export const BITVID_WEBSITE_URL = "https://bitvid.network/";
/**
* Optional external destination for the blog menu link.
*
* Sidebar navigation routes visitors to this URL when they select "Blog".
* Update the value to point at the long-form publishing platform you
* maintain for release notes or community updates. Leave empty (null or an
* empty string) to hide the Blog link entirely.
*/
export const BLOG_URL =
"https://habla.news/p/npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe";
/**
* Optional external destination for the community menu link.
*
* Configure this with a URL to your preferred community hub (e.g., Discord,
* Telegram, Flotilla). When unset (null or an empty string) the Community link
* is omitted from the sidebar dropup menu.
*/
export const COMMUNITY_URL = "https://groups.nip29.com/?relay=wss://relay.groups.nip29.com&groupId=d92ef5";
/**
* Optional external destination for the Nostr menu link.
*
* Point this at the profile or relay hub you want visitors to land on when
* they tap "Nostr" in the sidebar dropup. Leave empty (null or an empty
* string) to hide the link entirely.
*/
export const NOSTR_URL =
"https://primal.net/p/npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe";
/**
* Optional external destination for the GitHub menu link.
*
* Set this to the repository you want surfaced to operators and contributors.
* Leave empty (null or an empty string) to remove the GitHub entry from the
* sidebar dropup menu.
*/
export const GITHUB_URL = "https://github.com/PR0M3TH3AN/bitvid";
/**
* Optional external destination for the Beta menu link.
*
* Deployments that maintain a staging or beta environment can direct users to
* that surface here. Leave empty (null or an empty string) if there is no
* public beta to advertise.
*/
export const BETA_URL = "https://beta.bitvid.network/";
/**
* Optional external destination for the DNS menu link.
*
* Operators can expose their canonical DNS landing page here. Leave empty
* (null or an empty string) to hide the DNS entry from the sidebar.
*/
export const DNS_URL = "https://bitvid.network/";
/**
* Default image included in automated moderation DMs.
*
* BitVid embeds this media asset at the top of notification messages so the
* bitvid embeds this media asset at the top of notification messages so the
* payload renders with a recognizable preview. Provide a fully qualified URL
* that points to a hosted image accessible by the intended recipients.
*/
@@ -69,7 +145,7 @@ export const PLATFORM_FEE_PERCENT = 30;
* fallback Lightning address. When `PLATFORM_FEE_PERCENT` is greater than 0,
* this override also acts as the Lightning target for the platform’s split.
* Leave the value as `null` to respect the creator’s metadata and rely on the
* Super Admin profile publishing a `lud16` so BitVid still knows where to route
* Super Admin profile publishing a `lud16` so bitvid still knows where to route
* fees when they are enabled.
*/
export const PLATFORM_LUD16_OVERRIDE = "[email protected]";
@@ -78,7 +154,7 @@ export const PLATFORM_LUD16_OVERRIDE = "[email protected]";
* Optional list of relays to seed new sessions with instead of the defaults.
*
* Provide WSS URLs (e.g., `"wss://relay.example.com"`). Leave the array empty
* to keep BitVid’s bundled defaults. Operators that need a custom bootstrap set
* to keep bitvid’s bundled defaults. Operators that need a custom bootstrap set
* should list the relays in priority order; entries later in the list are used
* as fallbacks when earlier relays fail.
*/
@@ -99,17 +175,48 @@ export const ADMIN_WHITELIST_MODE_STORAGE_KEY = "bitvid_admin_whitelist_mode";
* Whether whitelist-only mode should be enabled the first time an operator
* loads the admin dashboard.
*
* Set this to `true` if you want BitVid to start with whitelist-only access and
* Set this to `true` if you want bitvid to start with whitelist-only access and
* require an explicit opt-out. Set to `false` to allow all creators by default.
* Operators can still toggle the mode at runtime; this value only controls the
* default state when no preference has been stored in localStorage yet.
*/
export const DEFAULT_WHITELIST_MODE_ENABLED = true;
/**
* Whether the public client should display content flagged as NSFW.
*
* When set to `false`, bitvid filters out any video notes marked with the
* `isNsfw` flag so they never appear in feeds or video lists. Toggle to `true`
* only if your deployment allows NSFW media to surface publicly.
*/
export const ALLOW_NSFW_CONTENT = false;
/**
* Optional theme accent overrides for light and dark mode.
*
* Operators can provide hex color strings (e.g., `"#2563eb"`) to customize the
* accent tokens without editing the generated CSS bundle. Leave values as
* `null` to keep the upstream defaults defined in `css/tokens.css`. Each theme
* accepts overrides for the base accent color as well as the stronger and
* pressed states used for hover/active presentations.
*/
export const THEME_ACCENT_OVERRIDES = Object.freeze({
light: Object.freeze({
accent: "#540011",
accentStrong: "#fe0032",
accentPressed: "#a90021",
}),
dark: Object.freeze({
accent: "#540011",
accentStrong: "#fe0032",
accentPressed: "#a90021",
}),
});
/**
* Nostr kind used when persisting watch history events.
*
* BitVid’s roadmap standardizes on kind 30079 so that watch events, view logs,
* bitvid’s roadmap standardizes on kind 30079 so that watch events, view logs,
* and media metadata stay in the same family of documents. During the rollout
* from the legacy 30078 payloads, clients query both kinds so historical data
* keeps syncing; remove the compatibility fetch once all writers emit the new
@@ -124,13 +231,13 @@ export const WATCH_HISTORY_KIND = 30079;
*
* This becomes the value of the `d` tag so clients can find the correct list
* without guessing. Customize the slug if you need isolation between multiple
* BitVid deployments that share relays, but keep it stable once clients begin
* bitvid deployments that share relays, but keep it stable once clients begin
* syncing history.
*/
export const WATCH_HISTORY_LIST_IDENTIFIER = "watch-history";
/**
* Legacy identifiers that BitVid clients should continue honoring when
* Legacy identifiers that bitvid clients should continue honoring when
* fetching historical watch-history snapshots.
*/
export const WATCH_HISTORY_LEGACY_LIST_IDENTIFIERS = Object.freeze([
@@ -144,7 +251,7 @@ export const WATCH_HISTORY_LEGACY_LIST_IDENTIFIERS = Object.freeze([
* deployments stick with the analytics-only view flow until operators opt in.
* To enable V2, set `window.__BITVID_RUNTIME_FLAGS__.FEATURE_WATCH_HISTORY_V2 = true`
* in a bootstrap script (or override the value before the app loads). When the
* flag stays off, BitVid still emits legacy view events and will honor
* flag stays off, bitvid still emits legacy view events and will honor
* existing watch-history reads per plan §12, but the sync UI will surface a
* disabled banner instead of querying relays.
*/
@@ -161,7 +268,7 @@ export const WATCH_HISTORY_MAX_ITEMS = 1500;
/**
* Whether clients should resolve watch-history entries in batches.
*
* When enabled, BitVid fetches video metadata in grouped queries instead of
* When enabled, bitvid fetches video metadata in grouped queries instead of
* issuing one request per entry. Operators with relays that struggle under
* bursty loads can disable batching at the cost of additional round trips.
*/
@@ -182,7 +289,7 @@ export const WATCH_HISTORY_BATCH_PAGE_SIZE = 10;
* Maximum size of the JSON payload for each watch-history chunk, measured
* before encryption.
*
* BitVid splits large histories across multiple events to stay within relay
* bitvid splits large histories across multiple events to stay within relay
* limits. This cap keeps individual chunks under roughly 60 KB so that
* encrypted payloads remain relay-friendly even after base64 expansion.
* Lower the number if your relays enforce tighter limits.
@@ -211,7 +318,7 @@ export const WATCH_HISTORY_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
/**
* How long the view counter should treat repeat plays as duplicates.
*
* BitVid de-duplicates view events that occur within this rolling window so
* bitvid de-duplicates view events that occur within this rolling window so
* quick refreshes or stalled replays do not inflate the totals. The default of
* 24 hours mirrors common analytics tooling, but you can tighten or relax the
* window depending on how aggressively you want to filter repeat traffic.
+97 -1
View File
@@ -7,6 +7,13 @@ import {
ADMIN_SUPER_NPUB,
PLATFORM_FEE_PERCENT,
PLATFORM_LUD16_OVERRIDE,
THEME_ACCENT_OVERRIDES,
BLOG_URL,
COMMUNITY_URL,
NOSTR_URL,
GITHUB_URL,
BETA_URL,
DNS_URL,
} from "./instance-config.js";
function assertSuperAdminConfigured(value) {
@@ -56,6 +63,50 @@ function getPlatformLud16Override(value) {
return value.trim();
}
const ACCENT_HEX_PATTERN = /^#(?:[0-9a-fA-F]{6})$/;
function assertThemeAccentOverrides(overrides) {
const source =
overrides && typeof overrides === "object"
? overrides
: THEME_ACCENT_OVERRIDES;
const allowedThemes = new Set(["light", "dark"]);
const allowedTokens = ["accent", "accentStrong", "accentPressed"];
Object.entries(source).forEach(([theme, themeOverrides]) => {
if (!allowedThemes.has(theme) || themeOverrides == null) {
return;
}
if (typeof themeOverrides !== "object") {
throw new Error(
`THEME_ACCENT_OVERRIDES.${theme} must be an object with accent overrides.`
);
}
allowedTokens.forEach((token) => {
const rawValue = themeOverrides[token];
if (rawValue == null || rawValue === "") {
return;
}
if (typeof rawValue !== "string") {
throw new Error(
`THEME_ACCENT_OVERRIDES.${theme}.${token} must be a hex string (e.g., '#2563eb').`
);
}
const trimmed = rawValue.trim();
if (!ACCENT_HEX_PATTERN.test(trimmed)) {
throw new Error(
`THEME_ACCENT_OVERRIDES.${theme}.${token} must be a #RRGGBB hex color.`
);
}
});
});
}
function resolveConfig(overrides = {}) {
return {
adminSuperNpub:
@@ -64,9 +115,46 @@ function resolveConfig(overrides = {}) {
overrides.PLATFORM_FEE_PERCENT ?? PLATFORM_FEE_PERCENT,
platformLud16Override:
overrides.PLATFORM_LUD16_OVERRIDE ?? PLATFORM_LUD16_OVERRIDE,
themeAccentOverrides:
overrides.THEME_ACCENT_OVERRIDES ?? THEME_ACCENT_OVERRIDES,
blogUrl: overrides.BLOG_URL ?? BLOG_URL,
communityUrl: overrides.COMMUNITY_URL ?? COMMUNITY_URL,
nostrUrl: overrides.NOSTR_URL ?? NOSTR_URL,
githubUrl: overrides.GITHUB_URL ?? GITHUB_URL,
betaUrl: overrides.BETA_URL ?? BETA_URL,
dnsUrl: overrides.DNS_URL ?? DNS_URL,
};
}
function assertOptionalHttpUrl(url, settingName) {
if (url == null) {
return null;
}
if (typeof url !== "string") {
throw new Error(`${settingName} must be a string when provided.`);
}
const trimmed = url.trim();
if (!trimmed) {
return null;
}
try {
const parsed = new URL(trimmed);
if (!parsed.protocol || !/^https?:$/.test(parsed.protocol)) {
throw new Error(`${settingName} must use http or https.`);
}
} catch (error) {
throw new Error(
`${settingName} must be a valid http(s) URL defined in config/instance-config.js.`,
{ cause: error },
);
}
return trimmed;
}
export function validateInstanceConfig(overrides) {
const config = resolveConfig(overrides);
assertSuperAdminConfigured(config.adminSuperNpub);
@@ -79,7 +167,15 @@ export function validateInstanceConfig(overrides) {
if (platformFeePercent > 0 && !trimmedOverride) {
throw new Error(
"PLATFORM_FEE_PERCENT is positive but PLATFORM_LUD16_OVERRIDE is empty. Set PLATFORM_LUD16_OVERRIDE to the Lightning address that should receive the platform's split, or publish a lud16 value on the Super Admin profile so BitVid can route platform fees."
"PLATFORM_FEE_PERCENT is positive but PLATFORM_LUD16_OVERRIDE is empty. Set PLATFORM_LUD16_OVERRIDE to the Lightning address that should receive the platform's split, or publish a lud16 value on the Super Admin profile so bitvid can route platform fees."
);
}
assertThemeAccentOverrides(config.themeAccentOverrides);
assertOptionalHttpUrl(config.blogUrl, "BLOG_URL");
assertOptionalHttpUrl(config.communityUrl, "COMMUNITY_URL");
assertOptionalHttpUrl(config.nostrUrl, "NOSTR_URL");
assertOptionalHttpUrl(config.githubUrl, "GITHUB_URL");
assertOptionalHttpUrl(config.betaUrl, "BETA_URL");
assertOptionalHttpUrl(config.dnsUrl, "DNS_URL");
}
+34 -11
View File
@@ -1,4 +1,4 @@
Welcome to **bitvid**, a decentralized video-sharing platform built on Nostr. These Community Guidelines outline the types of content allowed and prohibited on the client hosted at this domain. As bitvid is still in early access, enforcement will occur at the client level, meaning violations will result in content being blocked from display rather than removed from relays. These policies will evolve as we implement robust user blocking and reporting features.
Welcome to **bitvid**, a decentralized video-sharing platform built on Nostr. These Community Guidelines outline the types of content allowed and prohibited on the client hosted at this domain. Enforcement occurs at the **client level** with blur, hide, and downrank rules that mirror the current moderator tooling. Personal blocklists, mute controls, admin blacklists, and NIP-56 reporting are already available so you can tune your own experience.
> ⚠️ **Note:** Don't like the Guidelines? [Fork the code](https://github.com/PR0M3TH3AN/bitvid) and host the client on your own domain.
@@ -52,21 +52,44 @@ To maintain a functional and ethical platform, the following types of content ar
- **Deepfake impersonations** of real people used for deceptive purposes.
- AI-generated content must be **clearly labeled** to avoid misinformation.
## **4. Enforcement Actions**
## **4. Enforcement & Visibility Controls**
Until robust reporting and user moderation features are fully implemented, bitvid will enforce these guidelines at the **client level**:
bitvid enforces these guidelines through client-side visibility rules that match the current `moreMenuController` and `userBlocks` behavior:
- **Content Filtering:** Prohibited content will not be displayed within the client, though it may still exist on relays.
- **Account Restrictions:** Users violating guidelines may be **blocked from the whitelisted early access list**.
- **Reporting Mechanism:** Users are encouraged to report violations via NIP-56 reports, which will be reviewed manually.
- **Blurred previews:** When at least three F1 contacts file trusted NIP-56 `nudity` reports, thumbnails render blurred and autoplay previews are paused. Every blur includes a “show anyway” override.
- **Downranked authors:** Muting a creator adds them to your NIP-51 mute list (kind `10000`). Videos from muted creators are deprioritized but still visible with clear badges so you can reverse the action.
- **Personal blocks:** Blocking a creator adds them to your private block list (`userBlocks`). Videos from blocked creators are hidden from feeds, carousels, and modals unless you explicitly unblock them.
- **Admin hard hides:** Moderator-maintained blacklists (NIP-51 kind `30000`) remove creators from default feeds for everyone using the hosted client. Moderators can still review the content through “show anyway” controls when auditing decisions.
- **Relay transparency:** Because enforcement is client-side, blocked or blurred videos may still exist on relays and appear in other Nostr clients that do not apply these rules.
## **5. User Moderation Features (In Development)**
## **5. User Moderation Controls**
We are working on features that will give users more control over their experience, including:
You can manage moderation directly from the client using the existing menus rendered by `videoMenuRenderers`:
- **Personal blocklists** (prevent content from specific creators from appearing in feeds).
- **Community-curated moderation lists** (opt-in to trusted content curation groups).
- **Tag-based filtering** (users can toggle visibility of certain content categories).
### **5.1 Personal blocklist (`userBlocks`)**
- Open any video card or modal and select the **More** (`···`) button. Choose **Block creator** to hide all future uploads from that pubkey. `moreMenuController` will confirm the action and refresh your feeds.
- To review or remove blocks, open your profile avatar → **Profile** → **Blocked**. The list is private to your account; use **Unblock** next to a creator to restore their videos.
### **5.2 Mute list management**
- Use the same **More** menu and pick **Mute creator**. Muted creators stay visible but are downranked, aligning with the viewer mute list logic in `moreMenuController`.
- Return to a muted creator’s video later and select **Unmute creator** from the menu to restore their normal ranking.
### **5.3 Moderator blacklist controls**
- Moderators with admin permissions see **Blacklist creator** inside the **More** menu. This updates the shared admin list managed by `moreMenuController` and immediately removes the creator from default feeds.
- Only accounts with edit rights can add or remove creators. Attempting to blacklist without sufficient permissions will surface an error toast.
### **5.4 Reporting workflow (NIP-56)**
- Select **Report video** from the **More** menu to launch the reporting modal. Provide a category (e.g., `nudity`, `spam`, `illegal`) and submit; the client signs a NIP-56 report from your connected key.
- Reports feed the same trusted-report scoring used for blur and hide decisions. Moderators monitor these queues to escalate urgent cases.
### **5.5 Safety & Moderation thresholds**
- Open your profile avatar → **Profile** → **Safety & Moderation** to adjust blur and autoplay thresholds. Leave a field blank to return to the default (blur at ≥3 trusted reports, autoplay block at ≥2).
- These controls update immediately and sync with the moderation overlays applied by `moreMenuController` and `VideoCard` components.
## **6. Appeals and Feedback**
+9 -12
View File
@@ -19,27 +19,24 @@ Ready to jump in? Here's everything you need to know to start watching and shari
### Step 2: Prepare Your Video
1. Download WebTorrent Desktop app from [webtorrent.io/desktop](https://webtorrent.io/desktop/)
2. Open your video file in WebTorrent Desktop
3. It will create a special "magnet link" for your video
4. Keep WebTorrent Desktop running to share your video
1. Host your video at an HTTPS URL that browsers can stream directly (MP4/WebM/HLS/DASH all work). You can use your own CDN, Cloudflare Stream, an R2 bucket, or any static site host—as long as the link begins with `https://`.
2. (Optional but recommended) Generate a WebTorrent magnet so viewers can fall back to peer-to-peer delivery. Desktop apps like [WebTorrent Desktop](https://webtorrent.io/desktop/) or command-line tools can create the magnet for you.
3. Double-check that either the hosted URL or the magnet (or both) is ready before you open the Upload modal.
### Step 3: Share on bitvid
1. Click "Share a Video" on bitvid
2. Paste your video's magnet link
3. Add a title, description, and thumbnail
> ⚠️ **Note:** I recomend using [PostImage](https://postimages.org/) to host your image. You can copy the "Direct link" after uploading the image and use that link as your Thumbnail image.
4. Click "Post" to share!
1. Click "Share a Video" on bitvid to open the upload modal.
2. In **Custom** mode, enter your title, paste the hosted HTTPS URL, and (optionally) add the magnet plus any `ws`/`xs` hints. Switch to the experimental **Cloudflare** quick upload mode if you want bitvid to push the file straight to your Cloudflare R2 bucket—have your Account ID, S3 Access Key ID, and Secret Access Key ready (the advanced toggle also accepts an API token, Zone ID, and base domain if you use them).
3. Review the rest of the form: add a description and thumbnail, decide whether to allow comments, and set the NSFW or "For Kids" toggles to match your content. Explore the metadata section to fill in captions, duration, publish time, hashtags, participants, and other fields so first-time viewers have all the context they need.
4. Click "Post" to publish once everything looks right.
## Tips for Success
- Keep WebTorrent Desktop running while sharing videos
- Add eye-catching thumbnails to attract viewers
- Write clear descriptions to help people find your content
- Use the "Private" option if you only want to share with specific people
- After posting, open a card’s **More → Edit** action and use the [visibility toggle in the Edit Video modal](../components/edit-video-modal.html) to keep it private until you’re ready to share
- Encrypted watch-history sync lives in the [Watch History view](history.html), where you can clear devices or pause tracking whenever you need.
## Need Help?
+11 -14
View File
@@ -1,31 +1,31 @@
## Shipped
- **Private video filtering** – VideoListView now hides private uploads from shared feeds while still rendering them for their owners, keeping private sharing contained to the uploader’s views.
- **Edit & Upload modals** – Editing and publishing flows now rely on first-class modal forms with shared NIP-71 field management instead of browser pop-ups.
- **Universal more/settings menus** – Video cards expose consistent “more” and owner settings menus with channel navigation, sharing, blocking, and reporting actions wired through dedicated renderers.
- **Watch History view** – A dedicated watch history page ships with privacy messaging, pagination, relay sync, and local cache controls.
- **Zap controls** – Channel profiles manage zap visibility, login gating, retry flows, and receipt rendering via the Lightning controller on the page.
- **Source visibility filtering** – Lists now hide cards whose hosted URLs are offline unless their WebTorrent fallback is healthy, reusing the shared guard in `js/utils/cardSourceVisibility.js`.
## Bug Fixes
> ⚠️ **Note:** If you find a new bug thats not listed here. Please submit a [Bug Report](https://bitvid.network/?modal=bug).
- Speed up loading in subscriptions. Save to local cache?
- Fix "Dev Mode" publishing "Live Mode" notes—add a flag for dev mode posts.
- Fix issue with video post set to private.
- Disable "private video" until I can make it work better.
- Make "private video" work better by hiding cards from shared grids while keeping an owner-only view.
- Fix slow back button issues on Firefox.
- Add Amber login support for mobile.
- Add settings (three dots) button for all videos, not the same as gear menu. Only show delete and edit gear for logged-in users videos.
- Add support for "Playlist" lists and other custom lists (named whatever) and also "Watch Later" list and "Watch History".
- Add support for "Playlist" lists and other custom lists (named whatever) and also a "Watch Later" list.
- Add an "add to playlist" button to the edit button.
- Add "add to watch later" option to edit button.
- Add block button to settings button.
- Add report button to settings button.
- Fix autoplay broken on iPhone chrome.
- Fix playback broken on Safari on iPhone.
- Update note spec v3 transition to include new fields that have been added like "has previous" or "videoRootID" or enentID if null.
- Change "explore" view to "kids" view and add flag to all notes to see if they are for kids.
- Add "seed" lists.
- Make "edit" video a form modal rather than a browser pop-up.
- Add "upload thumbnail" as option to add/edit video form. (use https://apidocs.imgur.com/)
- Fix issue where page refreshes when pulling up a video in the video modal.
- Fix sidebar media query settings on medium-sized screens. (tablet/laptops)
- Fix various "text wrap" issues causing scroll left and right on profile and modal pages.
- Add zaps to profile and video modal pages.
- Add comments to video modal pages.
## Feature Additions
@@ -45,10 +45,7 @@
- Introduce a subscription mechanism with notifications.
- Add zaps to videos, profiles, and comments.
- Create a VRR (View, Rating, and Retention) Penalty Scoring system. [Click for more details](https://github.com/PR0M3TH3AN/bitvid/blob/main/content/roadmap/07_bitvid_Enhanced_View_Rating_%26_Retention_Penalty_Scoring.md)
- Implement visibility filtering for videos:
- Run lightweight URL-health probes so cards stay visible when the hosted playback link responds.
- When a URL is unreachable, fall back to WebTorrent and surface items only if their magnets can stream via peers or web seeds.
- Align the filtering logic with the player’s URL-first, WebTorrent-fallback behavior during list rendering.
- Expand visibility tooling around the shipped source guard to add operator overrides, richer status messaging, and better diagnostics when both URL and magnet sources fail.
- Dynamic Home Page and Video Tracking Specification. [Click for more details](https://github.com/PR0M3TH3AN/bitvid/blob/main/content/roadmap/08_bitvid_Enhanced_Dynamic_Home_Page_%26_Video_Tracking_Specification.md)
- Add multi-language support for content and filtration. (v4?)
- Create a settings menu for local account preferences, including relay, adult content, theme, and language.
@@ -253,7 +253,7 @@ export default VideoTracker;
import VideoTracker from './videoTracker.js';
class BitvidApp {
class bitvidApp {
constructor(nostrClient) {
this.nostrClient = nostrClient;
this.videoTracker = null;
@@ -276,7 +276,7 @@ class BitvidApp {
}
}
export default BitvidApp;
export default bitvidApp;
```
---
@@ -105,7 +105,7 @@ magnet:?xt=urn:btih:abc123def...&tr=wss%3A%2F%2Ftracker.webtorrent.io&tr=udp%3A%
```jsonc
["tracker", "wss://tracker.webtorrent.io"]
```
2. **Active WebRTC seeder**: Even with a `wss://` tracker, the torrent must be seeded in real time by at least one WebTorrent/WebRTC peer. Without a WebRTC seed, browser-based clients (like Bitvid or other WebTorrent-powered apps) cannot download or stream.
2. **Active WebRTC seeder**: Even with a `wss://` tracker, the torrent must be seeded in real time by at least one WebTorrent/WebRTC peer. Without a WebRTC seed, browser-based clients (like bitvid or other WebTorrent-powered apps) cannot download or stream.
3. **No `.torrent` file on Nostr**: This NIP is purely metadata (info hash, tracker URLs). Traditional BitTorrent clients can still use the magnet link if it has UDP/HTTP trackers.
@@ -202,4 +202,4 @@ Below is a more complete JSON example following NIP-35 conventions, demonstratin
### End of Updated Spec Sheet
This revision aims to integrate WebTorrent/WebRTC considerations seamlessly into the existing NIP-35 structure. It remains optional to provide `wss://` trackers, but doing so is strongly recommended if you want real-time streaming in browser-based clients like Bitvid or other WebTorrent-powered frontends.
This revision aims to integrate WebTorrent/WebRTC considerations seamlessly into the existing NIP-35 structure. It remains optional to provide `wss://` trackers, but doing so is strongly recommended if you want real-time streaming in browser-based clients like bitvid or other WebTorrent-powered frontends.
+8 -3
View File
@@ -1,3 +1,8 @@
:root{--color-page:#fff;--color-page-alt:#f9fafb;--color-surface:#0f172a;--color-surface-alt:#111827;--color-bg:var(--color-surface);--color-card:#1e293b;--color-card-private:#3b1d74;--color-card-critical:#3f0b13;--color-panel:#1f2937;--color-panel-hover:#334155;--color-border:#374151;--color-border-subtle:#d1d5db;--color-primary:#fe0032;--color-secondary:#ff93a5;--color-info:#60a5fa;--color-info-strong:#2563eb;--color-info-pressed:#1d4ed8;--color-muted-strong:#6b7280;--color-text:#f8fafc;--color-text-strong:#0f172a;--color-muted:#94a3b8;--color-muted-alt:#9ca3af;--color-subtle-text:#e2e8f0;--color-subtle:#e5e7eb;--color-critical:#7f1d1d;--color-critical-strong:#991b1b;--color-neutral-700:#3f3f46;--color-neutral-600:#52525b;--color-white:#fff;--color-black:#000;--color-overlay:#1a2234;--space-2xs:0.25rem;--space-xs:0.5rem;--space-sm:0.75rem;--space-md:1rem;--space-lg:1.5rem;--space-xl:2rem;--space-2xl:3rem;--radius-sm:0.25rem;--radius-md:0.5rem;--radius-lg:0.75rem;--radius-xl:1rem;--radius-full:9999px;--shadow-sm:0 0.0625rem 0.125rem 0 rgb(0 0 0/5%);--shadow-md:0 0.25rem 0.375rem -0.0625rem rgba(0,0,0,.1);--shadow-lg:0 0.625rem 0.9375rem -0.1875rem rgba(0,0,0,.1);--shadow-modal:0 1.25rem 2.5rem -0.375rem rgba(15,23,42,.55);--font-family-sans:"Inter","Segoe UI",system-ui,-apple-system,sans-serif;--font-family-mono:"Fira Code","SFMono-Regular",ui-monospace,monospace;--font-size-xs:0.75rem;--font-size-sm:0.875rem;--font-size-base:1rem;--font-size-lg:1.125rem;--font-size-xl:1.5rem;--font-size-2xl:2rem;--line-height-tight:1.1;--line-height-snug:1.25;--line-height-base:1.5;--line-height-relaxed:1.75;--motion-duration-fast:120ms;--motion-duration-base:160ms;--motion-duration-slow:280ms;--motion-ease-out:cubic-bezier(0.16,1,0.3,1);--motion-ease-in-out:cubic-bezier(0.4,0,0.2,1);--modal-motion-duration:var(--motion-duration-base)}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }
/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:var(--font-family-sans);font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:var(--font-family-mono);font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.\!container{width:100%!important}.container{width:100%}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.\!visible{visibility:visible!important}.visible{visibility:visible}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.bottom-1{bottom:.25rem}.bottom-4{bottom:1rem}.bottom-full{bottom:100%}.left-4{left:1rem}.right-0{right:0}.right-2{right:.5rem}.right-3{right:.75rem}.right-4{right:1rem}.top-0{top:0}.top-12{top:3rem}.top-14{top:3.5rem}.z-10{z-index:10}.z-20{z-index:20}.z-50{z-index:50}.z-\[60\]{z-index:60}.m-auto{margin:auto}.mx-1{margin-left:.25rem;margin-right:.25rem}.mx-2{margin-left:.5rem;margin-right:.5rem}.my-0{margin-top:0;margin-bottom:0}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-4{margin-top:1rem;margin-bottom:1rem}.my-6{margin-top:1.5rem;margin-bottom:1.5rem}.-ml-2{margin-left:-.5rem}.mb-0{margin-bottom:0}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.ml-auto{margin-left:auto}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mr-4{margin-right:1rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.box-border{box-sizing:border-box}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.aspect-video{aspect-ratio:16/9}.h-1{height:.25rem}.h-10{height:2.5rem}.h-12{height:3rem}.h-16{height:4rem}.h-2{height:.5rem}.h-4{height:1rem}.h-48{height:12rem}Line truncated
:root{--surface-base:var(--color-surface);--surface-alt:var(--color-surface-alt);--surface-raised:var(--color-card);--surface-raised-private:var(--color-card-private);--surface-raised-critical:var(--color-card-critical);--surface-panel:var(--color-panel);--surface-panel-hover:var(--color-panel-hover);--surface-sunken:var(--color-surface-sunken);--surface-muted:var(--color-surface-muted);--surface-veil:var(--color-overlay-veil);--surface-inverse:var(--color-page);--surface-overlay:var(--color-overlay);--surface-overlay-muted:var(--color-overlay-muted);--surface-overlay-strong:var(--color-overlay-strong);--surface-overlay-darker:var(--color-overlay-darker);--surface-overlay-black:var(--color-overlay-black);--surface-overlay-banner-base:var(--color-page);--surface-overlay-banner:var(--surface-overlay-banner-base);--surface-overlay-banner-opaque:var(--surface-overlay-banner-base);--surface-overlay-banner-strong:color-mix(in srgb,var(--surface-overlay-banner-base) 82%,transparent);--surface-overlay-banner-bottom:color-mix(in srgb,var(--surface-overlay-banner-base) 50%,transparent);--surface-overlay-banner-channel-mid:color-mix(in srgb,var(--surface-overlay-banner-base) 24%,transparent);--surface-overlay-banner-channel-faint:color-mix(in srgb,var(--surface-overlay-banner-base) 10%,transparent);--surface-overlay-banner-soft:color-mix(in srgb,var(--surface-overlay-banner-base) 56%,transparent);--surface-overlay-banner-faint:color-mix(in srgb,var(--surface-overlay-banner-base) 18%,transparent);--surface-overlay-banner-transparent:transparent;--surface-overlay-banner-gradient-header:linear-gradient(to bottom,var(--surface-overlay-banner-opaque) 0%,var(--surface-overlay-banner-strong) 38%,var(--surface-overlay-banner-soft) 70%,var(--surface-overlay-banner-faint) 88%,var(--surface-overlay-banner-transparent) 100%);--surface-overlay-banner-gradient-channel:linear-gradient(to top,var(--surface-overlay-banner-bottom) 0%,var(--surface-overlay-banner-channel-mid) 28%,var(--surface-overlay-banner-channel-faint) 42%,var(--surface-overlay-banner-transparent) 50%);--surface-overlay-banner-gradient:var(
--surface-overlay-banner-gradient-header
);--surface-overlay-panel:var(--color-overlay-panel);--surface-overlay-panel-soft:var(--color-overlay-panel-soft);--surface-overlay-panel-tint:var(--color-overlay-panel-tint);--surface-overlay-panel-glass:var(--color-overlay-panel-glass);--surface-overlay-panel-glass-soft:var(--color-overlay-panel-glass-soft);--surface-modal-backdrop:var(--color-modal-backdrop);--surface-modal:var(--color-modal-surface);--blur-modal-backdrop:18px;--blur-modal-alert:14px;--blur-thumbnail-blur:18px;--modal-nav-backdrop-blur:6px;--app-header-centerline-offset:calc(var(--space-sm) + var(--space-2xl-plus)/2);--border-default:var(--color-border);--border-subtle:var(--color-border-subtle);--border-strong:var(--color-border-translucent-stronger);--border-overlay:var(--color-border-translucent);--border-overlay-strong:var(--color-border-translucent-strongest);--border-overlay-glow:var(--color-border-translucent-glow);--border-neutral-soft:var(--color-border-neutral-soft);--border-info-soft:var(--color-border-info-soft);--border-sidebar-strong:var(--color-border-sidebar-strong);--border-width-hairline:1px;--border-width-fine:0.5px;--text-primary:var(--color-text);--text-inverse:var(--color-text-inverse);--text-muted:var(--color-text-muted);--text-muted-strong:var(--color-muted-strong);--text-subtle:var(--color-subtle-text);--text-frosted-primary:var(--color-text-frosted);--text-frosted-strong:var(--color-text-frosted-strong);--text-frosted-soft:var(--color-text-frosted-soft);--status-info:var(--color-info);--status-info-strong:var(--color-info-strong);--status-info-surface:var(--color-info-glass);--status-info-border:var(--color-info-border);--status-info-on:var(--color-info-on);--status-success:var(--color-success);--status-success-strong:var(--color-success-strong);--status-success-surface:var(--color-success-surface);--status-success-border:var(--color-success-border);--status-success-on:var(--color-success-on);--status-warning:var(--color-warning);--status-warning-strong:var(--color-warning-strong);--status-warning-surface:var(--color-warning-surface);--status-warning-border:var(--color-warning-border);--status-warning-on:var(--color-warning-on);--status-danger:var(--color-critical);--status-danger-strong:var(--color-critical-strong);--status-danger-surface:var(--color-danger-surface);--status-danger-border:var(--color-danger-border);--status-danger-on:var(--color-danger-on);--status-private:var(--color-private);--status-private-strong:var(--color-private-strong);--status-private-surface:var(--color-private-surface);--status-private-border:var(--color-private-border);--status-private-on:var(--color-private-on);--status-neutral:var(--color-muted);--status-neutral-strong:var(--color-muted-strong);--status-neutral-surface:var(--color-neutral-surface);--status-neutral-border:var(--color-neutral-border);--status-neutral-on:var(--color-neutral-on);--scrollbar-width:0.5rem;--scrollbar-track:var(--color-white-soft);--scrollbar-thumb:var(--color-white-softer);--scrollbar-thumb-hover:var(--color-white-softest);--z-app-header:40;--z-overlay-nav:60;--z-overlay-floating:70;--z-overlay-popover:140;--z-overlay-toast:180;--z-sidebar-overlay:10;--z-sidebar:20;--z-modal-root:100;--z-modal-overlay:110;--z-modal-content:120;--z-modal-player:130;--z-modal-top-root:140;--z-modal-top-overlay:150;--z-modal-top-content:160;--overlay-backdrop-color:var(--color-overlay-backdrop);--overlay-backdrop-blur:12px;--sidebar-width-expanded-base:16rem;--sidebar-dropup-content-width:0px;--sidebar-width-expanded:max(var(--sidebar-width-expanded-base),var(--sidebar-dropup-content-width));--sidebar-width-collapsed:4rem;--sidebar-width:var(--sidebar-width-expanded);--sidebar-control-size:2.75rem;--sidebar-content-gap:1rem;--space-sidebar-shell-inline:var(--space-lg);--space-sidebar-shell-block-start:var(--space-sm);--space-sidebar-shell-block-end:1.75rem;--space-sidebar-scroll-gap:1.75rem;--space-sidebar-panel-gap:1.35rem;--space-sidebar-panel-padding-block:1.35rem;--space-sidebar-panel-padding-inline:1.1rem;--space-sidebar-footer-gap:0.85rem;--space-sidebar-trigger-padding-block:0.78rem;--space-sidebar-popover-offset:0.85rem;--space-sidebar-popover-padding-block:0.85rem;--space-sidebar-popover-padding-inline:0.65rem;--space-sidebar-popover-gap:0.45rem;--space-sidebar-dropup-scroll-reserve:calc(var(--scrollbar-thin-width) + var(--space-sm) + var(--space-2xs));--sidebar-dropup-max-height:18rem;--space-sidebar-nav-padding-block:0.7rem;--space-sidebar-nav-padding-inline-start:var(--space-xs);--space-sidebar-nav-padding-inline-end:calc(var(--space-lg) + var(--space-2xs));--space-sidebar-subnav-padding-block:0.55rem;--space-sidebar-nav-translate:0.35rem;--grid-card-min-width:20rem;--popover-backdrop-blur:12px;--overlay-panel-radius:var(--radius-lg);--overlay-panel-shadow:var(--shadow-lg);--overlay-panel-padding-inline:var(--popup-padding-inline);--overlay-panel-padding-block:var(--popup-padding-block);--toast-translate:-1.5rem;--sidebar-mobile-gutter:3rem;--popup-padding-Line truncated
/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:var(--font-family-sans);font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:var(--font-family-mono);font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}html{font-size:80%}@media (min-width:768px){html{font-size:100%}}@media (min-width:1024px){html{font-size:75%}}*,:after,:before{scrollbar-width:thin;scrollbar-color:var(--scrollbar-thumb) var(--scrollbar-track)}::-webkit-scrollbar{width:var(--scrollbar-width);height:var(--scrollbar-width)}::-webkit-scrollbar-track{background:var(--scrollbar-track)}::-webkit-scrollbar-thumb{background:var(--scrollbar-thumb);border-radius:var(--radius-sm)}::-webkit-scrollbar-thumb:hover{background:var(--scrollbar-thumb-hover)}body{--app-header-centerline:calc(env(safe-area-inset-top, 0) + var(--app-header-centerline-offset));font-family:var(--font-family-sans);background-color:var(--color-page);color:var(--color-text-strong);line-height:1.5;margin:0;padding:0;overflow-x:hidden}#uiOverlay{position:fixed;inset:0;pointer-events:none;z-index:var(--z-overlay-popover)}#uiOverlay>*{pointer-events:auto}#editNip71TagsFieldset>legend,#nip71TagsFieldset>legend{margin-left:0;padding:0}#app{background-color:var(--color-page);color:inherit}body.modal-open,html.modal-open{overflow:hidden;height:100%}body.modal-open #app{overflow:hidden}body.modal-open #sidebar{pointer-events:none;opacity:0;visibility:hidden;z-index:var(--z-sidebar-overlay)}.ng-cloak,.ng-hide:not(.ng-hide-animate),.x-ng-cloak,[data-ng-cloak],[ng-cloak],[ng\:cloak],[x-ng-cloak]{display:none!important}ng\:form{display:block}.\!container{width:100%!important}.container{width:100%}@media (min-width:25rem){.\!container{max-width:25rem!important}.container{max-width:25rem}}@media (min-width:30rem){.\!container{max-width:30rem!important}.container{max-width:30rem}}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}#playerModal.ds-header{display:block}.ds-header{display:flex;align-items:center;justify-content:flex-start;padding-top:var(--space-sm);padding-bottom:var(--space-sm)}.app-shell__header{position:sticky;top:env(safe-area-inset-top,0);z-index:var(--z-app-header);padding-top:calc(var(--space-sm) + env(safe-area-inset-top, 0));padding-bottom:var(--space-sm);background-color:transparent;background-image:var(--surface-overlay-banner-gradient-header);background-repeat:no-repeat;background-size:100% 100%}.ds-metric-probe{position:absolute;tLine truncated
--player-modal-inline-padding,clamp(var(--space-md-plus),4vw,var(--space-2xl))
)}#playerModal .video-modal__primary>.watch-container:first-child{padding-block-start:var(--player-modal-vertical-padding)}#playerModal .video-modal__secondary>.watch-container:last-child{padding-block-end:var(--player-modal-vertical-padding)}#playerModal .modal-nav .watch-container{max-width:none;margin-inline:0}#playerModal .modal-info-panel{display:flex;flex-direction:column;gap:var(--space-lg)}#playerModal h2#videoTitle{margin:0;font-size:clamp(var(--font-size-xl),calc(var(--font-size-modal-title-base) + .5vw),var(--font-size-xl-plus));font-weight:600;line-height:1.3;color:var(--color-text)}#playerModal .modal-meta{font-size:var(--font-size-sm);line-height:1.4;color:var(--color-text-muted)}#playerModal .modal-view-count{font-weight:600;color:var(--color-text)}#playerModal .channel-card{display:flex;align-items:center;gap:var(--space-md);padding:var(--space-md-plus) var(--space-lg);border-radius:var(--radius-xl);border:var(--border-width-hairline) solid var(--color-border-translucent);background-color:var(--color-surface);box-shadow:var(--shadow-sm)}@media (max-width:30rem){#playerModal .modal-nav .icon-button{width:var(--space-xl);height:var(--space-xl);min-width:var(--space-xl);min-height:var(--space-xl)}#playerModal .watch-container{padding-inline:var(--space-sm)}#playerModal .channel-card{gap:var(--space-sm);padding:var(--space-sm) var(--space-md)}}#playerModal .channel-card__avatar{flex-shrink:0;width:var(--avatar-size-lg);height:var(--avatar-size-lg);border-radius:var(--radius-full);overflow:hidden;background-color:var(--color-border-subtle)}#playerModal .channel-card__avatar img{display:block;width:100%;height:100%;-o-object-fit:cover;object-fit:cover}#playerModal .channel-card__name{margin:0;font-size:var(--font-size-lg);font-weight:600;color:var(--color-text)}#playerModal .channel-card__npub{margin:0;font-size:var(--font-size-sm);color:var(--color-text-muted)}#playerModal .description-panel{padding:var(--space-md-plus) var(--space-lg);border-radius:var(--radius-xl);border:var(--border-width-hairline) solid var(--color-border-translucent);background-color:var(--color-surface);box-shadow:var(--shadow-sm)}#playerModal .description-panel p{margin:0;font-size:var(--font-size-sm);line-height:1.6;color:var(--color-text)}.player-modal__content{position:relative;z-index:var(--z-modal-content);display:flex;flex:1 1 auto;width:100%;min-height:100%;flex-direction:column;overflow:visible;padding:0;margin:0;border:none;border-radius:0;background-color:var(--color-surface);box-shadow:none}@media (prefers-reduced-motion:reduce){.player-modal__content{transition-property:none}}#playerModal .modal-nav{position:sticky;top:0;z-index:var(--z-overlay-nav);--tw-translate-y:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;background-color:var(--color-overlay-darker);background-image:linear-gradient(180deg,var(--color-overlay-panel-gradient-start) 0,var(--color-overlay-panel-gradient-end) 100%);border-bottom:var(--border-width-hairline) solid var(--color-border-translucent);backdrop-filter:blur(var(--modal-nav-backdrop-blur));-webkit-backdrop-filter:blur(var(--modal-nav-backdrop-blur))}#playerModal#playerModal .modal-nav__bar{display:block}#playerModal .modal-nav__bar{display:flex;width:100%;align-items:center;gap:.75rem;padding:.5rem}@media (min-width:640px){#playerModal .modal-nav__bar{padding-left:1rem;padding-right:1rem}}#playerModal .modal-nav__bar{padding-inline:var(
--player-modal-inline-padding,clamp(var(--space-md-plus),4vw,var(--space-2xl))
)}#playerModal#playerModal .modal-nav .modal-actions{display:block}#playerModal .modal-nav .modal-actions{display:flex;align-items:center;gap:.5rem;margin-left:auto}#playerModal .modal-nav .icon-button{display:inline-flex;align-items:center;justify-content:center;border-radius:var(--radius-full);color:var(--text-muted);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-timing-function:cubic-bezier(0,0,.2,1);width:var(--icon-size-lg);height:var(--icon-size-lg);background:var(--color-surface-tint-subtle);border:var(--border-width-hairline) solid var(--color-border-translucent-medium);color:var(--color-text-muted)}#playerModal .modal-nav .icon-button:focus-visible,#playerModal .modal-nav .icon-button:hover{background:var(--color-border-translucent-bright);border-color:var(--color-border-translucent-strong);color:var(--color-text)}.modal-nav--hidden{--tw-translate-y:-100%}.modal-nav--hidden,.modal-nav--visible{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.modal-nav--visible{--tw-translate-y:0px}.video-modal__layout{display:grid;gap:1.5rem}@media (min-width:1024px){.video-modal__layout{gap:2rem;grid-template-columns:minmax(0,var(--video-modal-primary-fr)) minmax(var(--video-modal-secondary-min),var(--video-modal-secondary-fr,1fr))}}.revert-modal__layout{display:grid;gap:1.5rem}@media (min-width:1024px){.revert-modal__layout{grid-template-columns:minmax(var(--revert-modal-sidebar-min),var(--revert-modal-sidebar-width)) minmax(0,1fr)}}#playerModal.video-modal__primary{display:block}.video-modal__primary{display:flex;flex-direction:column;gap:1.5rem}#playerModal.video-modal__secondary{display:block}.video-modal__secondary{display:flex;flex-direction:column;gap:1rem}.video-modal__video{position:relative;background-color:var(--color-black);padding-block:clamp(var(--space-xs),2.5vw,var(--space-md-plus))}@media (min-width:1024px){.video-modal__video{position:sticky;top:calc(var(--space-2xl) + var(--space-xl))}}#playerModal .video-modal__video video{border-radius:var(--radius-lg);box-shadow:var(--shadow-video-modal);max-height:min(80vh,56.25vw);-o-object-fit:contain;object-fit:contain}.video-modal__media{display:block;aspect-ratio:16/9;width:100%;-o-object-fit:contain;object-fit:contain}#playerModal.video-modal__details{display:block}.video-modal__details{display:flex;flex-direction:column;gap:1.5rem}.video-modal__meta{border-bottom-width:1px;border-color:var(--border-default);--tw-border-opacity:0.2;padding-bottom:1rem}#playerModal.video-modal__actions-row{display:block}.video-modal__actions-row{display:flex;flex-wrap:wrap;gap:.75rem}.video-modal__action-button,.video-modal__actions-row{align-items:center}.video-modal__action-button{display:inline-flex;height:2.75rem;justify-content:center;gap:.5rem;border-radius:var(--radius-full);font-size:var(--font-size-sm);line-height:var(--line-height-snug);font-weight:600;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(0,0,.2,1);min-width:var(--video-modal-action-min-width);padding-inline:var(--space-md);border-width:1px;border-style:solid}.bv-modal.video-modal__action-label,.profile-pane.video-modal__action-label,.video-modal__action-label{display:none}@media (min-width:640px){.video-modal__action-label{display:inline}}.video-modal__action-icon{height:1.25rem;width:1.25rem}.video-modal__channel,.video-modal__description,.video-modal__stats{border-radius:1rem;border-width:1px;border-color:var(--border-default);--tw-border-opacity:0.2;background-color:var(--surface-base);padding:1.25rem 1.5rem;--tw-shadow:var(--shadow-sm);--tw-shadow-colored:var(--shadow-sm);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.video-modal__description-body{overflow-wrap:anywhere;word-break:break-word}.video-modal__description-body a{color:var(--color-accent-strong);text-decoration:underline;text-decoration-color:currentColor;word-break:break-word}.video-modal__description-body a:focus-visible,.video-modal__description-body a:hover{color:var(--color-accent-pressed)}.video-modal__progress{height:.5rem;width:100%;overflow:hidden;border-radius:var(--radius-full);background-color:var(--border-subtle)}#playerModal.video-modal__stat-metrics{display:block}.video-modal__stat-metrics{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:.75rem;font-size:var(--font-size-sm);line-height:var(--line-height-snug);color:var(--text-muted)}.profile-modal{display:flexLine truncated
File diff suppressed because it is too large. Load diff
+664 -36
View File
@@ -1,45 +1,240 @@
:root {
/* Color tokens */
--color-page: rgb(255 255 255);
--color-page-alt: rgb(249 250 251);
--color-surface: rgb(15 23 42);
--color-surface-alt: rgb(17 24 39);
--color-bg: var(--color-surface);
--color-card: rgb(30 41 59);
--color-card-private: rgb(59 29 116);
--color-card-critical: rgb(63 11 19);
--color-panel: rgb(31 41 55);
--color-panel-hover: rgb(51 65 85);
--color-border: rgb(55 65 81);
--color-border-subtle: rgb(209 213 219);
--color-primary: rgb(254 0 50);
--color-secondary: rgb(255 147 165);
--color-info: rgb(96 165 250);
--color-info-strong: rgb(37 99 235);
--color-info-pressed: rgb(29 78 216);
--color-muted-strong: rgb(107 114 128);
--color-text: rgb(248 250 252);
--color-text-strong: rgb(15 23 42);
--color-muted: rgb(148 163 184);
--color-muted-alt: rgb(156 163 175);
--color-subtle-text: rgb(226 232 240);
--color-subtle: rgb(229 231 235);
--color-critical: rgb(127 29 29);
--color-critical-strong: rgb(153 27 27);
--color-neutral-700: rgb(63 63 70);
--color-neutral-600: rgb(82 82 91);
--color-white: rgb(255 255 255);
--color-black: rgb(0 0 0);
--color-overlay: rgb(26 34 52);
/* Semantic palette */
--surface-base: var(--color-surface);
--surface-alt: var(--color-surface-alt);
--surface-raised: var(--color-card);
--surface-raised-private: var(--color-card-private);
--surface-raised-critical: var(--color-card-critical);
--surface-panel: var(--color-panel);
--surface-panel-hover: var(--color-panel-hover);
--surface-sunken: var(--color-surface-sunken);
--surface-muted: var(--color-surface-muted);
--surface-veil: var(--color-overlay-veil);
--surface-inverse: var(--color-page);
--surface-overlay: var(--color-overlay);
--surface-overlay-muted: var(--color-overlay-muted);
--surface-overlay-strong: var(--color-overlay-strong);
--surface-overlay-darker: var(--color-overlay-darker);
--surface-overlay-black: var(--color-overlay-black);
--surface-overlay-banner-base: var(--color-page);
--surface-overlay-banner: var(--surface-overlay-banner-base);
--surface-overlay-banner-opaque: var(--surface-overlay-banner-base);
--surface-overlay-banner-strong: color-mix(
in srgb,
var(--surface-overlay-banner-base) 82%,
transparent
);
--surface-overlay-banner-bottom: color-mix(
in srgb,
var(--surface-overlay-banner-base) 50%,
transparent
);
--surface-overlay-banner-channel-mid: color-mix(
in srgb,
var(--surface-overlay-banner-base) 24%,
transparent
);
--surface-overlay-banner-channel-faint: color-mix(
in srgb,
var(--surface-overlay-banner-base) 10%,
transparent
);
--surface-overlay-banner-soft: color-mix(
in srgb,
var(--surface-overlay-banner-base) 56%,
transparent
);
--surface-overlay-banner-faint: color-mix(
in srgb,
var(--surface-overlay-banner-base) 18%,
transparent
);
--surface-overlay-banner-transparent: transparent;
--surface-overlay-banner-gradient-header: linear-gradient(
to bottom,
var(--surface-overlay-banner-opaque) 0%,
var(--surface-overlay-banner-strong) 38%,
var(--surface-overlay-banner-soft) 70%,
var(--surface-overlay-banner-faint) 88%,
var(--surface-overlay-banner-transparent) 100%
);
--surface-overlay-banner-gradient-channel: linear-gradient(
to top,
var(--surface-overlay-banner-bottom) 0%,
var(--surface-overlay-banner-channel-mid) 28%,
var(--surface-overlay-banner-channel-faint) 42%,
var(--surface-overlay-banner-transparent) 50%
);
--surface-overlay-banner-gradient: var(
--surface-overlay-banner-gradient-header
);
--surface-overlay-panel: var(--color-overlay-panel);
--surface-overlay-panel-soft: var(--color-overlay-panel-soft);
--surface-overlay-panel-tint: var(--color-overlay-panel-tint);
--surface-overlay-panel-glass: var(--color-overlay-panel-glass);
--surface-overlay-panel-glass-soft: var(--color-overlay-panel-glass-soft);
--surface-modal-backdrop: var(--color-modal-backdrop);
--surface-modal: var(--color-modal-surface);
--blur-modal-backdrop: 18px;
--blur-modal-alert: 14px;
--blur-thumbnail-blur: 18px;
--modal-nav-backdrop-blur: 6px;
--app-header-centerline-offset: calc(var(--space-sm) + (var(--space-2xl-plus) / 2));
--border-default: var(--color-border);
--border-subtle: var(--color-border-subtle);
--border-strong: var(--color-border-translucent-stronger);
--border-overlay: var(--color-border-translucent);
--border-overlay-strong: var(--color-border-translucent-strongest);
--border-overlay-glow: var(--color-border-translucent-glow);
--border-neutral-soft: var(--color-border-neutral-soft);
--border-info-soft: var(--color-border-info-soft);
--border-sidebar-strong: var(--color-border-sidebar-strong);
--border-width-hairline: 1px;
--border-width-fine: 0.5px;
--text-primary: var(--color-text);
--text-inverse: var(--color-text-inverse);
--text-muted: var(--color-text-muted);
--text-muted-strong: var(--color-muted-strong);
--text-subtle: var(--color-subtle-text);
--text-frosted-primary: var(--color-text-frosted);
--text-frosted-strong: var(--color-text-frosted-strong);
--text-frosted-soft: var(--color-text-frosted-soft);
--status-info: var(--color-info);
--status-info-strong: var(--color-info-strong);
--status-info-surface: var(--color-info-glass);
--status-info-border: var(--color-info-border);
--status-info-on: var(--color-info-on);
--status-success: var(--color-success);
--status-success-strong: var(--color-success-strong);
--status-success-surface: var(--color-success-surface);
--status-success-border: var(--color-success-border);
--status-success-on: var(--color-success-on);
--status-warning: var(--color-warning);
--status-warning-strong: var(--color-warning-strong);
--status-warning-surface: var(--color-warning-surface);
--status-warning-border: var(--color-warning-border);
--status-warning-on: var(--color-warning-on);
--status-danger: var(--color-critical);
--status-danger-strong: var(--color-critical-strong);
--status-danger-surface: var(--color-danger-surface);
--status-danger-border: var(--color-danger-border);
--status-danger-on: var(--color-danger-on);
--status-private: var(--color-private);
--status-private-strong: var(--color-private-strong);
--status-private-surface: var(--color-private-surface);
--status-private-border: var(--color-private-border);
--status-private-on: var(--color-private-on);
--status-neutral: var(--color-muted);
--status-neutral-strong: var(--color-muted-strong);
--status-neutral-surface: var(--color-neutral-surface);
--status-neutral-border: var(--color-neutral-border);
--status-neutral-on: var(--color-neutral-on);
/* Layout & stacking */
--scrollbar-width: 0.5rem;
--scrollbar-track: var(--color-white-soft);
--scrollbar-thumb: var(--color-white-softer);
--scrollbar-thumb-hover: var(--color-white-softest);
--z-app-header: 40;
--z-overlay-nav: 60;
--z-overlay-floating: 70;
--z-overlay-popover: 140;
--z-overlay-toast: 180;
--z-sidebar-overlay: 10;
--z-sidebar: 20;
--z-modal-root: 100;
--z-modal-overlay: 110;
--z-modal-content: 120;
--z-modal-player: 130;
--z-modal-top-root: 140;
--z-modal-top-overlay: 150;
--z-modal-top-content: 160;
--overlay-backdrop-color: var(--color-overlay-backdrop);
--overlay-backdrop-blur: 12px;
--sidebar-width-expanded-base: 16rem;
--sidebar-dropup-content-width: 0px;
--sidebar-width-expanded: max(
var(--sidebar-width-expanded-base),
var(--sidebar-dropup-content-width)
);
--sidebar-width-collapsed: 4rem;
--sidebar-width: var(--sidebar-width-expanded);
--sidebar-control-size: 2.75rem;
--sidebar-content-gap: 1rem;
--space-sidebar-shell-inline: var(--space-lg);
--space-sidebar-shell-block-start: var(--space-sm);
--space-sidebar-shell-block-end: 1.75rem;
--space-sidebar-scroll-gap: 1.75rem;
--space-sidebar-panel-gap: 1.35rem;
--space-sidebar-panel-padding-block: 1.35rem;
--space-sidebar-panel-padding-inline: 1.1rem;
--space-sidebar-footer-gap: 0.85rem;
--space-sidebar-trigger-padding-block: 0.78rem;
--space-sidebar-popover-offset: 0.85rem;
--space-sidebar-popover-padding-block: 0.85rem;
--space-sidebar-popover-padding-inline: 0.65rem;
--space-sidebar-popover-gap: 0.45rem;
--space-sidebar-dropup-scroll-reserve: calc(
var(--scrollbar-thin-width) + var(--space-sm) + var(--space-2xs)
);
--sidebar-dropup-max-height: calc(3rem * 6);
--space-sidebar-nav-padding-block: 0.7rem;
--space-sidebar-nav-padding-inline-start: var(--space-xs);
--space-sidebar-nav-padding-inline-end: calc(
var(--space-lg) + var(--space-2xs)
);
--space-sidebar-subnav-padding-block: 0.55rem;
--space-sidebar-nav-translate: 0.35rem;
--grid-card-min-width: 20rem;
--popover-backdrop-blur: 12px;
--overlay-panel-radius: var(--radius-lg);
--overlay-panel-shadow: var(--shadow-lg);
--overlay-panel-padding-inline: var(--popup-padding-inline);
--overlay-panel-padding-block: var(--popup-padding-block);
--toast-translate: -1.5rem;
--sidebar-mobile-gutter: 3rem;
--popup-padding-inline: 1rem;
--popup-padding-block: 0.75rem;
--video-modal-primary-fr: 1.85fr;
--video-modal-secondary-min: 260px;
--video-modal-secondary-fr: 1fr;
--revert-modal-sidebar-min: 240px;
--revert-modal-sidebar-width: 280px;
--popover-inline-safe-max: calc(100vw - var(--space-xl));
/* Progress primitives */
--progress-height-sm: 0.25rem;
--progress-height-md: 0.5rem;
--progress-radius: var(--radius-full);
--progress-track-color: var(--color-border-subtle);
--progress-fill-color: var(--color-info);
--progress-transition-duration: var(--motion-duration-fast);
--progress-transition-ease: var(--motion-ease-out);
/* Spacing scale */
--space-4xs: 0.0625rem;
--space-3xs: 0.125rem;
--space-2xs: 0.25rem;
--space-xs: 0.5rem;
--space-xs-snug: 0.4rem;
--space-sm: 0.75rem;
--space-md: 1rem;
--space-md-plus: 1.25rem;
--space-lg: 1.5rem;
--space-xl: 2rem;
--space-xl-compact: 2.25rem;
--space-xl-plus: 2.5rem;
--space-2xl: 3rem;
--space-2xl-plus: 3.5rem;
/* Border radii */
--radius-sm: 0.25rem;
@@ -47,26 +242,124 @@
--radius-lg: 0.75rem;
--radius-xl: 1rem;
--radius-full: 9999px;
--radius-modal-xl: 1.75rem;
--radius-sidebar-shell: 1.5rem;
--radius-sidebar-panel: 1.1rem;
--radius-sidebar-nav: 0.95rem;
--radius-toggle-thumb: 0.5625rem;
/* Component sizing */
--menu-min-width: 12rem;
--modal-max-width: 62.5rem;
--modal-sheet-inline-max: var(--modal-max-width);
--modal-shell-max-height: 90vh;
--modal-sheet-max-height: 92vh;
--modal-body-max-height: calc(92vh - 4rem);
--modal-pane-max-height: 48vh;
--modal-pane-min-height: 20rem;
--profile-modal-nav-min-width: max-content;
--profile-modal-nav-max-width: max-content;
--profile-modal-scrollbar-offset: var(--space-2xs);
--modal-embed-height: 80vh;
--profile-history-max-height: 26.25rem;
--status-log-min-height: 5rem;
--layout-player-max-width: 68.75rem;
--layout-docs-max-width: 92.5rem;
--icon-size-sm: var(--font-size-lg);
--icon-size-md: var(--space-md-plus);
--icon-size-lg: var(--space-xl-plus);
--avatar-size-lg: var(--space-2xl-plus);
--form-textarea-min-height: 8rem;
--scrollbar-thin-width: 0.4rem;
--modal-progress-height: calc(var(--space-3xs) * 3);
--video-modal-action-min-width: 2.75rem;
--outline-thick-width: 0.1875rem;
--icon-button-ring-width: var(--outline-thick-width);
/* Form controls */
--form-select-icon-offset: var(--space-sm);
--form-select-icon-gap: var(--space-xs-snug);
--form-select-icon-hit-area: var(--space-xl-plus);
--form-select-icon-size: 0.6rem;
--form-select-divider-width: var(--border-width-hairline);
--form-checkbox-checkmark-offset: calc(var(--space-3xs) * -1);
/* Utility measurements */
--offscreen-offset: -9999px;
/* Motion */
--motion-card-enter-translate: var(--space-xs);
--motion-fade-in-translate: var(--space-md-plus);
/* Shadows */
--shadow-sm: 0 0.0625rem 0.125rem 0 rgb(0 0 0 / 5%);
--shadow-md: 0 0.25rem 0.375rem -0.0625rem rgb(0 0 0 / 10%);
--shadow-lg: 0 0.625rem 0.9375rem -0.1875rem rgb(0 0 0 / 10%);
--shadow-modal: 0 1.25rem 2.5rem -0.375rem rgb(15 23 42 / 55%);
--shadow-sm: 0 0.0625rem 0.125rem 0 var(--color-shadow-floating);
--shadow-md: 0 0.25rem 0.375rem -0.0625rem var(--color-shadow-floating);
--shadow-lg: 0 0.625rem 0.9375rem -0.1875rem var(--color-shadow-elevated);
--shadow-modal: 0 2.75rem 6rem -1.25rem var(--color-shadow-modal);
--shadow-sidebar-shell: 0 1.875rem 3.75rem -1.5rem var(--color-shadow-sidebar);
--shadow-sidebar-accent: 0 1.65rem 3.25rem -1.35rem var(--color-sidebar-glow);
--shadow-sidebar-trigger: 0 1.25rem 2.5rem -1.35rem var(--color-shadow-panel);
--shadow-sidebar-dropup: 0 1.75rem 3.25rem -1.45rem var(--color-sidebar-glow);
--shadow-sidebar-panel: 0 2.25rem 4.5rem -1.85rem var(--color-shadow-sidebar);
--shadow-sidebar-focus-ring: 0 0 0 0.125rem var(--color-sidebar-glow),
0 0 0 0.25rem var(--color-sidebar-hover-strong);
--shadow-popover-zap: 0 22px 48px -18px var(--color-shadow-intense);
--shadow-video-modal: 0 1rem 2.25rem var(--color-shadow-elevated);
/* Typography */
--font-family-sans: "Inter", "Segoe UI", system-ui, -apple-system, sans-serif;
--font-family-mono: "Fira Code", "SFMono-Regular", ui-monospace, monospace;
--font-size-4xs: 0.65rem;
--font-size-3xs: 0.6875rem;
--font-size-2xs: 0.7rem;
--font-size-xs: 0.75rem;
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.125rem;
--font-size-xl: 1.5rem;
--font-size-2xl: 2rem;
--font-size-xl-plus: 1.875rem;
--font-size-modal-title-base: 1.25rem;
--font-size-sidebar-nav: 0.95rem;
--font-size-sidebar-subnav: 0.85rem;
--font-size-sidebar-trigger: 0.9rem;
--line-height-tight: 1.1;
--line-height-snug: 1.25;
--line-height-base: 1.5;
--line-height-relaxed: 1.75;
--tracking-extra-wide: 0.2em;
/* Beacon UI tokens */
--grid-border-width: calc(var(--space-2xs) / 4);
--grid-canvas-padding-top: var(--grid-border-width);
--grid-viewport-min-height: calc(var(--space-xl) - var(--space-sm));
--grid-cell-padding-block: var(--space-sm);
--grid-cell-padding-inline: var(--space-md);
--grid-header-font-size: var(--font-size-xs);
--grid-header-letter-spacing: 0.08em;
--grid-overlay-margin: 10%;
--grid-overlay-padding-block: var(--space-md);
--grid-overlay-padding-inline: var(--space-xl);
--grid-overlay-radius: var(--radius-lg);
--grid-overlay-font-size: var(--font-size-lg);
--grid-overlay-text-opacity: 0.66;
--grid-resizer-width: calc(var(--space-2xs) * 1.25);
--grid-resize-overlay-width: var(--grid-border-width);
--grid-sort-indicator-size: calc(var(--space-lg) - var(--space-2xs));
--grid-sort-indicator-offset: var(--space-xs);
--grid-sort-indicator-stroke: calc(var(--space-2xs) / 2);
--grid-sort-indicator-thickness: var(--grid-border-width);
--notify-padding-block: var(--space-md);
--notify-padding-inline: var(--space-2xl);
--notify-font-size: var(--font-size-base);
--notify-dismiss-size: calc(var(--space-xl) + var(--space-xs));
--notify-dismiss-size-compact: calc(var(--space-lg) + var(--space-2xs));
--notify-dismiss-font-size: var(--font-size-sm);
--notify-dismiss-radius: var(--radius-full);
--notify-dismiss-color: var(--color-subtle);
--notify-dismiss-background: var(--color-overlay-panel);
--notify-dismiss-background-hover: var(--color-overlay-panel-soft);
--z-notify: 1200;
/* Motion */
--motion-duration-fast: 120ms;
@@ -76,3 +369,338 @@
--motion-ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--modal-motion-duration: var(--motion-duration-base);
}
:root[data-theme="light"] {
/* Light theme palette */
--color-page: rgb(247 248 252);
--color-page-alt: rgb(242 244 248);
--color-surface: rgb(255 255 255);
--color-surface-alt: rgb(248 250 252);
--color-bg: var(--color-surface);
--color-card: rgb(255 255 255);
--color-card-private: rgb(243 236 255);
--color-card-critical: rgb(255 236 238);
--color-panel: rgb(252 253 255);
--color-panel-hover: rgb(239 242 249);
--color-border: rgb(210 219 236);
--color-border-subtle: rgb(229 234 242);
--color-accent: rgb(99 102 241);
--color-accent-strong: rgb(79 70 229);
--color-accent-pressed: rgb(67 56 202);
--color-primary: var(--color-accent-strong);
--color-secondary: var(--color-accent);
--color-logo-accent: var(--color-accent-strong);
--color-logo-wordmark: rgb(40 54 77);
--color-logo-wordmark-inverse: var(--color-white);
--color-logo-background: rgb(15 23 42);
--color-logo-background-inverse: var(--color-white);
--color-info: rgb(59 130 246);
--color-info-strong: rgb(37 99 235);
--color-info-pressed: rgb(29 78 216);
--color-info-glass: rgb(59 130 246 / 0.14);
--color-info-border: rgb(59 130 246 / 0.35);
--color-info-on: rgb(227 242 255);
--color-muted-strong: rgb(71 85 105);
--color-text: rgb(24 31 44);
--color-text-strong: rgb(12 16 26);
--color-text-inverse: rgb(255 255 255);
--color-text-muted: rgb(102 112 133);
--color-muted: rgb(136 146 166);
--color-muted-alt: rgb(120 130 150);
--color-subtle-text: rgb(136 146 166);
--color-subtle: rgb(243 245 250);
--color-surface-sunken: rgb(232 235 244);
--color-surface-muted: rgb(236 240 249);
--color-critical: rgb(220 54 70);
--color-critical-strong: rgb(185 32 55);
--color-danger-surface: rgb(252 226 230);
--color-danger-border: rgb(220 54 70 / 0.35);
--color-danger-on: rgb(96 18 31);
--color-warning: rgb(234 179 8);
--color-warning-strong: rgb(202 138 4);
--color-warning-surface: rgb(253 224 71 / 0.35);
--color-warning-border: rgb(234 179 8 / 0.35);
--color-warning-glass: rgb(253 224 71 / 0.18);
--color-warning-on: rgb(92 55 2);
--color-success: rgb(22 163 74);
--color-success-strong: rgb(21 128 61);
--color-success-surface: rgb(222 250 236);
--color-success-border: rgb(34 197 94 / 0.3);
--color-success-on: rgb(18 84 48);
--color-private: rgb(139 92 246);
--color-private-strong: rgb(124 58 237);
--color-private-surface: rgb(237 233 254);
--color-private-border: rgb(139 92 246 / 0.32);
--color-private-on: rgb(74 29 150);
--color-neutral-700: rgb(71 85 105);
--color-neutral-600: rgb(102 112 133);
--color-neutral-surface: rgb(232 236 244);
--color-neutral-border: rgb(205 213 226);
--color-neutral-on: rgb(62 72 92);
--color-white: rgb(255 255 255);
--color-black: rgb(0 0 0);
--color-overlay: rgb(15 23 42 / 0.3);
--color-overlay-black: rgb(15 23 42 / 0.7);
--color-overlay-muted: rgb(60 75 105 / 0.18);
--color-overlay-strong: rgb(15 23 42 / 0.56);
--color-overlay-darker: rgb(12 18 32 / 0.68);
--color-overlay-panel: rgb(248 250 255 / 0.92);
--color-overlay-panel-soft: rgb(244 247 255 / 0.78);
--color-overlay-panel-tint: rgb(146 170 211 / 0.24);
--color-overlay-panel-glass: rgb(255 255 255 / 0.85);
--color-overlay-panel-glass-soft: rgb(246 249 255 / 0.7);
--color-overlay-panel-gradient-start: rgb(255 255 255 / 0.95);
--color-overlay-panel-gradient-end: rgb(232 236 244 / 0.92);
--color-modal-backdrop: rgb(15 23 42 / 0.28);
--color-modal-surface: rgb(255 255 255 / 0.94);
--color-overlay-veil: rgb(103 112 133 / 0.45);
--color-overlay-banner-bottom: rgb(255 255 255 / 0.6);
--color-overlay-banner-transparent: rgb(255 255 255 / 0);
--color-overlay-backdrop: rgb(15 23 42 / 0.2);
--scrollbar-track: color-mix(
in srgb,
var(--color-sidebar-border-strong) 40%,
transparent
);
--scrollbar-thumb: color-mix(
in srgb,
var(--color-accent) 64%,
transparent
);
--scrollbar-thumb-hover: color-mix(
in srgb,
var(--color-accent-strong) 76%,
transparent
);
--color-sidebar-surface: rgb(255 255 255 / 0.9);
--color-sidebar-surface-alt: rgb(236 240 247 / 0.72);
--color-sidebar-border: rgb(184 194 212 / 0.28);
--color-sidebar-border-strong: rgb(148 163 184 / 0.36);
--color-sidebar-hover: color-mix(in srgb, var(--color-accent) 16%, transparent);
--color-sidebar-hover-strong: color-mix(in srgb, var(--color-accent-strong) 24%, transparent);
--color-sidebar-glow: color-mix(in srgb, var(--color-accent) 36%, transparent);
--color-sidebar-accent-start: color-mix(in srgb, var(--color-accent-strong) 88%, transparent);
--color-sidebar-accent-end: color-mix(in srgb, var(--color-accent) 82%, transparent);
--color-sidebar-text-muted: var(--color-text-muted);
--sidebar-icon-filter: brightness(0) saturate(100%);
--color-surface-tint-subtle: rgb(15 23 42 / 0.08);
--color-border-translucent: rgb(148 163 184 / 0.18);
--color-border-translucent-strong: rgb(148 163 184 / 0.22);
--color-border-translucent-bright: rgb(148 163 184 / 0.28);
--color-border-translucent-stronger: rgb(148 163 184 / 0.32);
--color-border-translucent-strongest: rgb(148 163 184 / 0.36);
--color-border-translucent-glow: rgb(148 163 184 / 0.45);
--color-border-translucent-medium: rgb(148 163 184 / 0.2);
--color-border-neutral-soft: rgb(148 163 184 / 0.25);
--color-border-info-soft: rgb(59 130 246 / 0.32);
--color-border-sidebar-strong: rgb(102 112 133 / 0.75);
--color-ring-private: rgb(139 92 246 / 0.5);
--color-ring-private-hover: rgb(168 85 247 / 0.6);
--color-ring-critical: rgb(220 54 70 / 0.35);
--color-ring-critical-soft: rgb(220 54 70 / 0.3);
--color-ring-critical-hover: rgb(220 54 70 / 0.4);
--color-ring-critical-strong: rgb(220 54 70 / 0.5);
--color-ring-alert: rgb(239 68 68 / 0.48);
--color-gradient-visibility-start: rgb(99 102 241 / 0.28);
--color-gradient-visibility-end: rgb(129 140 248 / 0.24);
--color-white-soft: rgb(255 255 255 / 0.08);
--color-white-softer: rgb(255 255 255 / 0.24);
--color-white-softest: rgb(255 255 255 / 0.32);
--color-shadow-floating: rgb(15 23 42 / 0.18);
--color-shadow-elevated: rgb(15 23 42 / 0.16);
--color-shadow-panel: rgb(15 23 42 / 0.12);
--color-shadow-notification: rgb(15 23 42 / 0.14);
--color-shadow-intense: rgb(15 23 42 / 0.22);
--color-shadow-sidebar: rgb(15 23 42 / 0.28);
--color-shadow-modal: rgb(15 23 42 / 0.28);
--color-divider-strong: rgb(203 213 225 / 0.6);
--color-text-frosted-strong: rgb(40 54 77 / 0.85);
--color-text-frosted: rgb(61 74 98 / 0.8);
--color-text-frosted-soft: rgb(82 96 122 / 0.72);
}
:root[data-theme="dark"],
:root:not([data-theme]) {
/* Dark theme palette */
--color-page: rgb(6 10 18);
--color-page-alt: rgb(8 12 22);
--color-surface: rgb(10 14 26);
--color-surface-alt: rgb(14 20 34);
--color-bg: var(--color-surface);
--color-card: rgb(18 26 44);
--color-card-private: rgb(38 28 62);
--color-card-critical: rgb(58 20 28);
--color-panel: rgb(16 24 40);
--color-panel-hover: rgb(24 34 52);
--color-border: rgb(61 80 112);
--color-border-subtle: rgb(38 52 76);
--color-accent: rgb(129 140 248);
--color-accent-strong: rgb(99 102 241);
--color-accent-pressed: rgb(79 70 229);
--color-primary: var(--color-accent-strong);
--color-secondary: rgb(56 189 248);
--color-logo-accent: var(--color-accent-strong);
--color-logo-wordmark: rgb(198 207 222);
--color-logo-wordmark-inverse: var(--color-white);
--color-logo-background: rgb(12 18 30);
--color-logo-background-inverse: var(--color-white);
--color-info: rgb(96 165 250);
--color-info-strong: rgb(56 132 255);
--color-info-pressed: rgb(37 99 235);
--color-info-glass: rgb(56 132 255 / 0.22);
--color-info-border: rgb(96 165 250 / 0.42);
--color-info-on: rgb(235 244 255);
--color-muted-strong: rgb(206 215 232);
--color-text: rgb(226 232 240);
--color-text-strong: rgb(244 247 255);
--color-text-inverse: rgb(12 18 30);
--color-text-muted: rgb(165 179 201);
--color-muted: rgb(148 163 184);
--color-muted-alt: rgb(128 139 162);
--color-subtle-text: rgb(168 180 205);
--color-subtle: rgb(28 38 58);
--color-surface-sunken: rgb(13 19 32 / 0.92);
--color-surface-muted: rgb(18 26 44 / 0.82);
--color-critical: rgb(239 83 98);
--color-critical-strong: rgb(220 38 38);
--color-danger-surface: rgb(220 38 38 / 0.28);
--color-danger-border: rgb(220 38 38 / 0.45);
--color-danger-on: rgb(255 228 233);
--color-warning: rgb(250 204 21);
--color-warning-strong: rgb(234 179 8);
--color-warning-surface: rgb(253 224 71 / 0.22);
--color-warning-border: rgb(250 204 21 / 0.42);
--color-warning-glass: rgb(253 224 71 / 0.2);
--color-warning-on: rgb(26 18 0);
--color-success: rgb(34 197 94);
--color-success-strong: rgb(22 163 74);
--color-success-surface: rgb(22 163 74 / 0.24);
--color-success-border: rgb(34 197 94 / 0.45);
--color-success-on: rgb(210 252 228);
--color-private: rgb(168 85 247);
--color-private-strong: rgb(139 92 246);
--color-private-surface: rgb(124 58 237 / 0.26);
--color-private-border: rgb(168 85 247 / 0.45);
--color-private-on: rgb(239 230 255);
--color-neutral-700: rgb(58 75 110);
--color-neutral-600: rgb(90 105 133);
--color-neutral-surface: rgb(99 114 144 / 0.24);
--color-neutral-border: rgb(99 114 144 / 0.35);
--color-neutral-on: rgb(210 220 238);
--color-white: rgb(255 255 255);
--color-black: rgb(0 0 0);
--color-overlay: rgb(8 11 20 / 0.72);
--color-overlay-black: rgb(0 0 0 / 0.88);
--color-overlay-muted: rgb(12 18 30 / 0.6);
--color-overlay-strong: rgb(8 12 24 / 0.82);
--color-overlay-darker: rgb(5 8 16 / 0.9);
--color-overlay-panel: rgb(14 20 34 / 0.92);
--color-overlay-panel-soft: rgb(18 26 42 / 0.78);
--color-overlay-panel-tint: rgb(56 93 160 / 0.36);
--color-overlay-panel-glass: rgb(14 24 42 / 0.86);
--color-overlay-panel-glass-soft: rgb(16 28 48 / 0.72);
--color-overlay-panel-gradient-start: rgb(18 26 44 / 0.92);
--color-overlay-panel-gradient-end: rgb(10 16 30 / 0.92);
--color-modal-backdrop: rgb(5 8 14 / 0.68);
--color-modal-surface: rgb(14 22 38 / 0.88);
--color-overlay-veil: rgb(4 6 12 / 0.94);
--color-overlay-banner-bottom: rgb(0 0 0 / 0.6);
--color-overlay-banner-transparent: rgb(0 0 0 / 0);
--color-overlay-backdrop: rgb(5 8 14 / 0.75);
--scrollbar-track: color-mix(
in srgb,
var(--color-sidebar-surface-alt) 54%,
transparent
);
--scrollbar-thumb: color-mix(
in srgb,
var(--color-accent) 72%,
transparent
);
--scrollbar-thumb-hover: color-mix(
in srgb,
var(--color-accent-strong) 84%,
transparent
);
--color-sidebar-surface: rgb(12 18 30 / 0.94);
--color-sidebar-surface-alt: rgb(10 18 32 / 0.8);
--color-sidebar-border: rgb(86 106 142 / 0.26);
--color-sidebar-border-strong: rgb(112 133 170 / 0.42);
--color-sidebar-hover: color-mix(in srgb, var(--color-accent) 22%, transparent);
--color-sidebar-hover-strong: color-mix(in srgb, var(--color-accent-strong) 32%, transparent);
--color-sidebar-glow: color-mix(in srgb, var(--color-accent) 42%, transparent);
--color-sidebar-accent-start: color-mix(in srgb, var(--color-accent) 92%, transparent);
--color-sidebar-accent-end: color-mix(in srgb, var(--color-accent-strong) 88%, transparent);
--color-sidebar-text-muted: var(--color-text-muted);
--sidebar-icon-filter: brightness(0) invert(1);
--color-surface-tint-subtle: rgb(94 113 161 / 0.12);
--color-border-translucent: rgb(99 114 144 / 0.22);
--color-border-translucent-strong: rgb(110 130 170 / 0.28);
--color-border-translucent-bright: rgb(148 163 184 / 0.35);
--color-border-translucent-stronger: rgb(148 163 184 / 0.38);
--color-border-translucent-strongest: rgb(198 207 222 / 0.45);
--color-border-translucent-glow: rgb(129 140 248 / 0.55);
--color-border-translucent-medium: rgb(99 114 144 / 0.28);
--color-border-neutral-soft: rgb(99 114 144 / 0.3);
--color-border-info-soft: rgb(96 165 250 / 0.35);
--color-border-sidebar-strong: rgb(32 46 68 / 0.88);
--color-ring-private: rgb(168 85 247 / 0.55);
--color-ring-private-hover: rgb(192 132 252 / 0.65);
--color-ring-critical: rgb(239 83 98 / 0.42);
--color-ring-critical-soft: rgb(239 83 98 / 0.35);
--color-ring-critical-hover: rgb(239 83 98 / 0.5);
--color-ring-critical-strong: rgb(220 38 38 / 0.6);
--color-ring-alert: rgb(220 38 38 / 0.6);
--color-gradient-visibility-start: rgb(129 140 248 / 0.4);
--color-gradient-visibility-end: rgb(192 132 252 / 0.32);
--color-white-soft: rgb(255 255 255 / 0.1);
--color-white-softer: rgb(255 255 255 / 0.3);
--color-white-softest: rgb(255 255 255 / 0.4);
--color-shadow-floating: rgb(8 11 30 / 0.55);
--color-shadow-elevated: rgb(8 11 30 / 0.45);
--color-shadow-panel: rgb(15 23 42 / 0.35);
--color-shadow-notification: rgb(15 23 42 / 0.3);
--color-shadow-intense: rgb(250 204 21 / 0.48);
--color-shadow-sidebar: rgb(0 0 0 / 0.45);
--color-shadow-modal: rgb(8 11 30 / 0.55);
--color-divider-strong: rgb(15 23 42 / 0.35);
--color-text-frosted-strong: rgb(226 232 240 / 0.94);
--color-text-frosted: rgb(226 232 240 / 0.9);
--color-text-frosted-soft: rgb(226 232 240 / 0.82);
}
+43
View File
@@ -0,0 +1,43 @@
# Beacon vendor CSS migration
This inventory captures the selectors that mattered in `torrent/dist/beacon.vendor.css` and how they now map to design-system driven rules. The goal is to keep future tweaks aligned with shared tokens rather than re-introducing ad-hoc styles or vendor bundles.
> **Contributor note:** Beacon CSS updates must follow the [“Styling & Theming Rules (token-first)” guidance in `AGENTS.md`](../../AGENTS.md#styling--theming-rules-token-first) and the [README CSS build pipeline](../../README.md#css-build-pipeline). Run the relevant scripts before committing changes—at minimum `npm run lint:css`, `npm run build:css`, `npm run check:css`, and `npm run build:beacon` to ensure the Tailwind output and bundled assets stay in sync with token rules.
## Angular UI Grid
| Legacy selector(s) | Tailwind component mapping | Notes |
| --- | --- | --- |
| `.ui-grid`, `.ui-grid-render-container`, `.ui-grid-contents-wrapper` | `.ui-grid` block in `css/tailwind.source.css` | Uses shared border/box styling via `--grid-border-width`, preserving the translated container shell. |
| `.ui-grid-top-panel-background`, `.ui-grid-footer-panel-background`, `.ui-grid-header`, `.ui-grid-top-panel` | Same block | Backgrounds and dividers now pull from `var(--color-surface-alt)` and `var(--color-border-translucent-medium)`. |
| `.ui-grid-header-viewport`, `.ui-grid-footer-viewport`, `.ui-grid-header-canvas`, `.ui-grid-footer-canvas`, `.ui-grid-header-cell-wrapper`, `.ui-grid-footer-cell-wrapper`, `.ui-grid-header-cell-row`, `.ui-grid-footer-cell-row` | Same block | Table layout and overflow behaviour carried over verbatim. |
| `.ui-grid-header-cell`, `.ui-grid-footer-cell`, `.ui-grid-header-cell .sortable`, `.ui-grid-header-cell .ui-grid-sort-priority-number` | Same block | Header typography now keyed off `--grid-header-font-size` and `--grid-header-letter-spacing`; sortable affordances reference spacing tokens. |
| `.ui-grid-sortarrow`, `.ui-grid-sortarrow.down`, (icon font triangle) | `.ui-grid-sortarrow` + pseudo-element | Arrow is now rendered with CSS borders driven by `--grid-sort-indicator-*` tokens, eliminating the ui-grid icon font. |
| `.ui-grid-vertical-bar` + child selectors | Same block | Divider widths reuse the shared border width token. |
| `.ui-grid-viewport`, `.ui-grid-canvas` | Same block | Min heights and padding resolve through grid tokens instead of literal px. |
| `.ui-grid-row`, row parity/hover/last-child selectors, `.ui-grid-row-selected > [ui-grid-row] > .ui-grid-cell` | Same block | Alternating row fills reference surface tokens and preserve selection colours. |
| `.ui-grid-cell`, `.ui-grid-cell:last-child`, `.ui-grid-cell-contents`, `.ui-grid-cell-contents-hidden` | Same block | Cell padding now uses `--grid-cell-padding-*`; hidden content helper retained. |
| `.ui-grid-cell-focus`, `.ui-grid-focuser`, `.ui-grid-offscreen` | Same block | Focus handling switches to tokenised shadows while keeping accessibility helpers. |
| `.ui-grid-no-row-overlay`, `.ui-grid-no-row-overlay > *` | Same block | Empty-state panel adopts design-system surface/background tokens. |
| `.ui-grid-menu-button`, `.ui-grid-menu` | Same block | Menu chrome now uses shared spacing, shadows, and border tokens. |
| `.ui-grid-column-resizer`, `.ui-grid.column-resizing`, `.ui-grid.column-resizing .ui-grid-resize-overlay` | Same block | Resizer hit boxes use `--grid-resizer-width`; overlay colour references translucent border tokens. |
| `.ui-grid-row-saving .ui-grid-cell`, `.ui-grid-row-dirty .ui-grid-cell`, `.ui-grid-row-error .ui-grid-cell` | Same block | Status colours mapped to `var(--color-muted-strong)`, `var(--color-warning-strong)`, and `var(--color-critical-strong)`. |
| `.ui-grid-disable-selection` | Same block | Still prevents text selection with consistent cursor feedback. |
| `.ui-grid-animate-spin`, `@keyframes ui-grid-spin` | Same block | Animation now hooks into the global motion duration token. |
> **Omitted vendor selectors:** Column menus, tree, filter icons, and other ui-grid extension classes remain unused in the beacon routes, so the icon font `@font-face` payload was intentionally dropped.
## ng-notify
| Legacy selector(s) | Tailwind component mapping | Notes |
| --- | --- | --- |
| `.ngn`, `.ngn.ngn-visible`, `.ngn.ngn-enter-active`, `.ngn.ngn-leave-active` | `.ngn` block in `css/tailwind.source.css` | Layout, typography, and z-index now rely on `--notify-*` and `--z-notify`. Pointer-events handling mirrors vendor behaviour. |
| `.ngn-top`, `.ngn-bottom`, `.ngn-component` | Same block | Placement helpers rewritten with logical properties (`inset-block-*`). |
| `.ngn-dismiss`, `.ngn-sticky .ngn-dismiss`, `.ngn-dismiss:hover`, `.ngn-dismiss:active` | Same block | Close button sizing derives from tokens; hover/active states use `color-mix` against overlay tokens. |
| `.ngn-info`, `.ngn-error`, `.ngn-success`, `.ngn-warn`, `.ngn-grimace` | Same block | Severity palettes tie directly into existing info/warning/critical tokens. |
| `.ngn-prime.*`, `.ngn-pastel.*`, `.ngn-pitchy.*` | Same block | Theme variants blend design-system colours via `color-mix`, preserving tonal differences without hard-coded hex values. |
| `@media (max-width: 30rem)` overrides | Same block | Compact sizing switches to shared spacing and font-size tokens. |
## Font faces
The legacy bundle shipped three `@font-face` blocks for the `ui-grid` icon font. Sort indicators and menu affordances now render through CSS borders and shared tokens, so the icon font is no longer required or embedded in the Tailwind build.
+7
View File
@@ -0,0 +1,7 @@
# Deployment Notes
## Design system rollout
- **Status:** The design system styling is always active. The former `FEATURE_DESIGN_SYSTEM` runtime flag has been removed, so there is no rollback toggle during deployment.
- **Template contract:** Every layout, view, and partial must keep `data-ds` on its root element. The entrypoint stamps `data-ds="new"` during bootstrap so controllers inherit the correct primitives.
- **Controllers:** UI controllers still receive a `designSystem` context, but `context.isNew()` now always returns `true`. Use the context for consistency, but do not branch on legacy styling paths.
+477
View File
@@ -0,0 +1,477 @@
# bitvid Design System Primitives
bitvid's visual language is now powered by Tailwind utilities backed by our design tokens. This document lists the primitives available via `css/tailwind.source.css`, how they behave across states, and when to mix them with Tailwind utilities.
All templates and embeds must load `css/tailwind.generated.css`; that compiled bundle is the only stylesheet shipped with the app. Remove any lingering references to legacy `tailwind.min.css` assets when porting surfaces into the design system.
> **UI Styling:** For day-to-day decisions, start with the [Styling & Theming Rules](../AGENTS.md#styling--theming-rules-token-first) in `AGENTS.md` and the [CSS build pipeline overview](../README.md#css-build-pipeline). Run `npm run lint:css` before committing so design-token regressions are caught early.
## Runtime contract
The design system is now the canonical styling path across bitvid—there is no runtime flag or rollback toggle. Every template renders with `data-ds="new"` on its root container, and controllers always receive the "new" mode from the `designSystem` context (see `js/designSystem.js`). When migrating surfaces, ensure their root nodes include `data-ds` so the entrypoint can stamp the attribute during bootstrap.
## Dynamic runtime styling
Inline styles are blocked in production builds, so runtime components that need bespoke coordinates or measurements must route their overrides through `js/designSystem/dynamicStyles.js`.
- `registerScope(id, selectors)` creates (or reuses) a constructable stylesheet when supported, allocates a unique identifier, and injects empty rules scoped to `[data-ds-style-id="<id>"]`. Callers attach the attribute to any element that should inherit those rules.
- `setVariables(id, tokens)` updates CSS custom properties on the registered scope. Pass CSS variable names (for example, `"--ds-metric-probe-length"`) and string or number values—`setVariables` handles serialization and removes entries when they resolve to empty strings.
- `releaseScope(id)` removes the generated rules and frees the identifier. Always invoke it from component teardown paths to avoid leaking rules between renders.
Design-system metrics lean on this helper to keep runtime measurements inline-style free:
- `readDesignTokenAsPixels` measures non-`px`/`rem` tokens with an offscreen probe that ships in core CSS (`.ds-metric-probe`). The probe is registered through the helper so dynamic lengths are fed into `--ds-metric-probe-length` without touching inline styles.
When introducing a new runtime feature that requires dynamic styling:
1. Register a dedicated scope during setup and attach the returned attribute to the relevant DOM node.
2. Update custom properties with `setVariables` instead of touching `.style` or appending `<style>` tags.
3. Clean up by calling `releaseScope` when the component unmounts.
4. Prefer reusable primitives (like `.ds-metric-probe`) for offscreen measurement or layout shims so multiple components can share the same CSS.
## Typography & Layout Tokens
Small-type utilities and modal geometry are now backed by dedicated semantic tokens in `css/tokens.css`. The scale introduces
`--font-size-4xs` (0.65rem), `--font-size-3xs` (0.6875rem), `--font-size-2xs` (0.7rem), and `--tracking-extra-wide`
(0.2em) alongside modal measurements such as `--modal-shell-max-height`, `--modal-sheet-max-height`,
`--modal-body-max-height`, `--modal-pane-max-height`, and `--profile-history-max-height`. Tailwind exposes the tokens through
utility aliases including `text-4xs`, `text-3xs`, `text-2xs`, `tracking-extra-wide`, `max-h-modal-shell`, `max-h-modal-sheet`,
`max-h-modal-body`, `max-h-modal-pane`, and `max-h-profile-history`. Use these utilities instead of arbitrary values so future
scale adjustments can ship from a single source of truth.
### Migration notes
- Replace literal classes such as `text-[11px]`, `text-[0.7rem]`, and `text-[0.65rem]` with the new semantic helpers
(`text-3xs`, `text-2xs`, `text-4xs`). The controller logic for saved profiles and modal renderers has been updated to follow
the same pattern.
- Swap bespoke tracking overrides (for example, `tracking-[0.2em]`) to `tracking-extra-wide` so the widened CTA headings stay
consistent across themes.
- Move modal shells and scroll containers off arbitrary `max-h-[...]` utilities in favour of `max-h-modal-shell`,
`max-h-modal-sheet`, `max-h-modal-body`, `max-h-modal-pane`, and `max-h-profile-history`.
- Keep the legacy search strings (like `text-[11px]`) in regression checklists—repositories should no longer include those
selectors after migrating a template or controller.
### Tailwind arbitrary values lint
Run `npm run lint:tailwind-brackets` after touching markup or prose that embeds HTML. The lint walks `.html`, `.njk`, and `.md`
sources and fails when Tailwind's bracket syntax introduces hard-coded measurements (`text-[11px]`, `max-w-[18rem]`, etc.).
- Prefer the semantic utilities shipped alongside the tokens (`max-w-popover-safe`, `max-h-modal-shell`, `text-3xs`).
- When you truly need an arbitrary value, drive it with a design token by referencing the CSS custom property (`w-[var(--layout-player-max-width)]`, `min-h-[var(--form-textarea-min-height)]`).
- Update the allowlist sparingly—every entry should correspond to a documented token so future adjustments cascade automatically.
The lint shares the same underlying script as `npm run lint:tokens`, so both commands stay in lockstep with the design-system
guardrails.
## Theming
### `[data-theme]` contract
The default experience renders the production dark palette. Pages and embeds can opt into alternates by setting the `data-theme` attribute on the `<html>` (or any ancestor that wraps the component). `data-theme="light"` promotes the light tokens defined in `css/tokens.css`, swapping surface, panel, border, muted, and overlay colors while keeping semantic accents consistent. Omit the attribute or set it back to `default` to inherit the dark baseline. Reserve additional theme keys (for example, `data-theme="contrast"`) for upcoming accessibility palettes—the same hook will activate them once their tokens land. When scoping components inside legacy templates, prefer wrapping the smallest possible container so downstream shims continue to function.
### Token overrides & experiments
Tokens are sourced from `css/tokens.css` and consumed as CSS custom properties. To try temporary overrides—such as custom backgrounds for a partner landing page—stack a higher-specificity selector and reassign the variable without hard-coding literal colors:
```css
.landing[data-theme="light"] {
--color-bg-page: var(--color-bg-surface);
--shadow-card: 0 10px 40px rgba(12, 14, 23, 0.25);
}
```
Limit overrides to variables defined in `css/tokens.css` and document any long-lived changes in this file so the design tokens remain the source of truth. Feature-flagged experiments should wrap overrides in a `data-flag-<name>` selector or toggle them via controller logic so the defaults remain stable when the flag is off.
## Semantic Palette
All interactive surfaces now draw from a shared semantic color map exposed through `css/tokens.css` and Tailwind utility aliases. The palette is grouped into text, surface, overlay, and status slots so controllers can swap state styles without reaching for raw Tailwind hues:
- **Text:** `text-primary`, `text-inverse`, `text-muted`, `text-muted-strong`, and `text-subtle` map to `--text-*` tokens. Use them for copy, helper text, and frosted overlays instead of `text-gray-*`.
- **Surfaces:** `bg-surface`, `bg-surface-alt`, `bg-surface-raised`, `bg-surface-muted`, and `bg-overlay-*` wrap cards, modals, and scrims. Pair them with `border-surface`, `border-overlay`, or `border-overlay-strong` when rendering shells.
- **Statuses:** `status-*` tokens expose matching text, background, and border slots. Tailwind utilities include `text-status-success`, `bg-status-warning-surface`, `border-status-danger-border`, and chip helpers (`.pill`, `.badge`) that accept `data-variant="info" | "success" | "warning" | "critical" | "private" | "neutral"`.
- **Overlays:** `.overlay-scrim` and `.overlay-panel` helpers consume the overlay tier tokens for modals, sheets, and popovers. Adjust the strength via `data-strength`/`data-variant` rather than applying translucent grays. Blur states now ride semantic variables—reuse `--blur-modal-backdrop` for scrims, `--blur-modal-alert` for inline notices, and `--blur-thumbnail-blur` for frosted imagery instead of hard-coded pixel values.
### Migration guidance
- Swap raw Tailwind palette utilities (for example, `text-gray-400`, `bg-amber-500/10`, `border-red-700/70`) for the matching semantic utilities above. The new lint (`npm run lint:tailwind-colors`) fails when legacy color classes appear outside deprecated directories.
- Replace ad-hoc chips (`inline-flex ... border-red-700/70 bg-red-900/40`) with `.pill` or `.badge` plus a `data-variant`. These helpers automatically pull the correct status text/border/fill tokens.
- Use `text-info`/`hover:text-info-strong` for links that previously relied on `text-blue-400` so anchors adapt in both light and dark modes.
- When rendering tinted descriptions or placeholders, prefer `text-muted`, `text-muted-strong`, or `text-subtle` over semi-transparent whites. Token overrides keep the same classes usable across the default and light themes.
### Layout & spacing extensions
The core spacing scale gained intermediate stops for tighter micro-layouts and larger hero treatments. `css/tokens.css` now exports `--space-4xs`, `--space-3xs`, `--space-xs-snug`, `--space-md-plus`, `--space-xl-compact`, `--space-xl-plus`, and `--space-2xl-plus`; Tailwind surfaces matching utilities such as `p-4xs`, `p-3xs`, `px-xs-snug`, `py-md-plus`, `gap-xl-compact`, `px-xl-plus`, and `gap-2xl-plus`. Pair them with the new layout primitives—`--menu-min-width` (`min-w-menu`), `--modal-max-width` (`max-w-modal`), `--layout-player-max-width` (`max-w-player`), and `--layout-docs-max-width` (`max-w-docs`)—to anchor consistent breakpoints across popovers, modals, and documentation shells without reintroducing raw `rem`/`px` literals.
### Previewing themes
The design-system kitchen sink (`docs/kitchen-sink.html`) exposes a theme switcher that mirrors the production toggle contract. Use the "Theme" select menu in the header to flip between `default`, `light`, and any preview keys (for example, `contrast`). This control updates `document.documentElement.dataset.theme` at runtime, letting you verify hover/focus/disabled states without redeploying. When authoring new palettes, add them to `css/tokens.css`, confirm the kitchen-sink toggle picks them up, and record before/after screenshots for review so the CI snapshots have clear context.
## Focus Handling
All interactive primitives mix in the shared `.focus-ring` helper. It removes default outlines and applies a branded ring using `focus-visible` only. When building bespoke components, add `.focus-ring` to your root interactive element or replicate its rules:
```html
<button class="focus-ring inline-flex items-center gap-2 ...">Label</button>
```
The helper includes:
- `focus-visible:ring-2` with `ring-info` and `ring-opacity-70`
- `focus-visible:ring-offset-2` with an offset on `bg-surface`
- `focus-visible:transition-shadow` using the tokenized `duration-fast` easing curve
Never hide focus styles without providing an accessible alternative.
## Motion Guardrails
### Motion inventory
The `rg "transition"` audit surfaces the components that animate by default. Keep this list in sync when new motion primitives land:
- **Buttons:** `.btn` and `.btn-ghost` transition their backgrounds, borders, and shadows using the shared token durations. 【F:css/tailwind.source.css†L18-L55】
- **Cards and watch history:** `.card`, `.watch-history-card`, and their nested controls use hover/focus transitions plus optional entry animations via `data-motion="enter"`. 【F:css/tailwind.source.css†L238-L804】
- **Popovers:** `.popover__panel` fades and scales between `[data-state]` values. 【F:css/tailwind.source.css†L119-L141】
- **Modals:** `.bv-modal__panel`, `.video-modal__panel`, and `.player-modal__content` animate in concert with the modal nav/header layout helpers. 【F:docs/kitchen-sink.html†L335-L391】【F:css/tailwind.source.css†L385-L640】
- **Feedback & loading:** `.progress-bar-fill`, `.status-spinner--inline`, and `.status-banner .status-spinner` communicate state changes with width transitions or spin animations. 【F:css/tailwind.source.css†L907-L948】
- **Sidebar controls:** `.sidebar-nav-link`, `.sidebar-dropup-trigger`, `.sidebar-collapse-toggle`, and related chevrons/toggles translate, fade, and resize during rail expansion. 【F:css/tailwind.source.css†L1164-L1598】
### Shadow inventory
- `--shadow-popover-zap` keeps the zap tip elevation consistent across themes; apply it to `.popover__panel[data-variant="zap"]` or related toast surfaces when you need the intense popover glow. 【F:css/tokens.css†L406-L415】【F:css/tailwind.source.css†L293-L298】
- `--shadow-video-modal` anchors modal playback chrome so large embeds reuse the same lift as `#playerModal .video-modal__video video`. 【F:css/tokens.css†L406-L415】【F:css/tailwind.source.css†L1374-L1378】
- `--color-shadow-intense` still exposes the raw color for bespoke drop-shadow recipes—pair it with `drop-shadow-intense` utilities when only the hue needs to change. 【F:css/tokens.css†L93-L101】【F:tailwind.config.cjs†L28-L32】【F:tailwind.config.cjs†L147-L157】
### Reduced-motion policy
- A consolidated `@media (prefers-reduced-motion: reduce)` block now zeroes out transition and animation durations globally, removes motion-only transforms, and leaves opacity state changes in place so surfaces still show and hide instantly. 【F:css/tailwind.source.css†L1766-L1981】
- Motion tokens also collapse to `0s` inside Tailwind's component layer so the primitives (`.btn`, `.card`, `.popover__panel`, watch-history tiles) inherit the same behaviour without per-selector overrides. 【F:css/tailwind.source.css†L229-L270】
- Spinners fall back to static indicators when reduced motion is requested, while focus rings remain intact because the underlying styles are unaffected—only the timing curves are reset. 【F:css/tailwind.source.css†L1778-L1981】
- When adding a new animated component, register its selector in this block (or reuse the tokenised durations) so reduced-motion users never see unintended transitions. Document the addition here alongside the inventory.
## Component Primitives
### Buttons
| Class | Description |
| --- | --- |
| `.btn` | Primary action button (`bg-primary`, white text). Hover lifts to `bg-info-strong`, focus pushes to `bg-info-pressed`, disabled reduces opacity. |
| `.btn-ghost` | Framed secondary action. Transparent by default with subtle border, fills with `bg-panel-hover` on hover/focus. |
Both buttons:
- Use `.focus-ring` internally
- Include pointer-blocking and opacity changes on `:disabled`
- Are uppercase with wide tracking to match our existing CTA styling
Mix in Tailwind utilities for icon spacing (`gap-3`), sizing (`px-lg`, `py-sm`), or layout (`w-full`). Avoid overriding colors unless introducing a new semantic variant.
#### Variants & Toggle States
- Apply `data-variant="critical"` to `.btn-ghost` when rendering destructive affordances (for example, "Remove variant" pills inside repeater rows). The primitive swaps the border and text colors to the critical palette and deepens the fill on hover/focus.
- Toggle groups can rely on `[aria-pressed="true"]` or `data-state="active"` without extra utilities. When a `.btn-ghost` reports an active state it automatically promotes to the filled primary style.
```html
<div class="inline-flex items-center rounded-full bg-panel/80 p-1" role="group">
<button class="btn-ghost flex-1" type="button" aria-pressed="true">Custom</button>
<button class="btn-ghost flex-1" type="button" aria-pressed="false">Cloudflare</button>
</div>
```
### Badges
`.badge` renders an inline pill (`bg-panel`, uppercase text). Opt into semantic variants with `data-variant="info"`, `data-variant="critical"`, or `data-variant="neutral"`. Layer on utilities for icon alignment or truncation.
### Cards
`.card` is the base surface for grid items and panels:
- Rounded corners (`rounded-lg`) and `bg-card`
- Token shadows on hover
- Focus states reuse `.focus-ring`
- Accepts `data-state="private" | "critical" | "disabled"` for special treatments
Combine with padding utilities per layout. Interactive cards should maintain `cursor-pointer` manually (see legacy shim example below) until templates adopt dedicated controller classes.
> **2025-10-11 update:** The legacy `.video-card*` shim selectors were removed. Video grids should now render plain `.card` elements and opt into semantic variants via `data-state`, `data-alert`, and `data-motion` attributes. Runtime code no longer maps the deprecated classes, so older selectors will not receive token updates.
### Form Inputs
`.form-control` is the base primitive for text entry surfaces. It handles layout (`block`, `w-full`), rounded corners, subtle borders, muted placeholders, and the shared focus ring. Hovering deepens the border, focus highlights with `border-info-strong`, `:invalid` raises a critical border, and `:disabled` mutes the field while blocking pointer events.
Specialisations reuse `.form-control`:
- `.input` – Standard single-line text input.
- `.textarea` – Multi-line input with a minimum height (`min-h-[var(--form-textarea-min-height)]`) and `resize-y` enabled.
- `.select` – Native `<select>` element with `appearance-none`, an integrated arrow glyph, and consistent spacing (`pr-10`).
All three share hover, focus-visible, disabled, and invalid treatments. Layer on utilities (`h-12`, `text-lg`, icon padding) as needed without re-declaring tokens.
Controllers can mark read-only fields by setting `data-state="locked"` alongside the existing `readonly` attribute. The attribute shifts the background to `bg-overlay-panel-soft`, deepens the border (`border-border-sidebar-strong`), and suppresses the focus ring so locked inputs stay visually distinct without falling back to `!important` overrides. The same hook works on `.input`, `.textarea`, `.select`, and any raw `.form-control` element—disabled states keep the locked palette.
Arrow and checkbox adornments now lean entirely on tokens: `--form-select-icon-offset`, `--form-select-icon-gap`, `--form-select-icon-hit-area`, and `--form-select-icon-size` position the native select chevron, while `--form-checkbox-checkmark-offset` keeps the checkmark aligned at any scale. Textareas pull their baseline from `--form-textarea-min-height`, and thin scrollbars reuse the shared `--scrollbar-thin-width` so embedded panels feel consistent across browsers.
```html
<form class="bv-stack bv-stack--tight" aria-labelledby="zapAmountLabel">
<label
id="zapAmountLabel"
class="text-xs font-semibold uppercase tracking-wide text-muted-strong"
>
Zap amount (sats)
</label>
<input
id="zapAmountInput"
type="number"
min="1"
step="1"
class="input"
placeholder="Enter sats"
/>
<button class="btn" type="submit">Send</button>
</form>
```
For toggles:
- `.checkbox` resets native appearance, applies the token border/background, and swaps to the info palette when checked. The checkmark is rendered via `::after`, and disabled states lower contrast while keeping the focus ring behaviour.
- `.switch` renders a pill track with a translating thumb (`::before`). Apply `[aria-checked="true"]` or `.is-on` to move the thumb and recolor the track. Motion respects `prefers-reduced-motion` while maintaining the focus ring.
### Modal Surface
- `.bv-modal` anchors fixed overlays (full-viewport flexbox centered layout).
- `.bv-modal-backdrop` applies the tokenized overlay color with blur.
- `.bv-modal__panel` hosts modal content (`bg-surface`, `shadow-modal`, focus ring).
Sizing now comes from dedicated tokens: `max-w-modal` and `rounded-modal-xl` map to `--modal-max-width` and `--radius-modal-xl`, while icon controls reuse `--icon-size-lg`, `--icon-size-md`, and `--icon-button-ring-width` for consistent hit targets. Progress indicators pick up `--modal-progress-height`, and the player modal inherits `max-w-player` so hero metadata never exceeds `--layout-player-max-width`.
Use `--modal-sheet-inline-max` when you need a sheet to breathe on narrow viewports without exceeding the standard modal width. The token resolves to the smaller of `90vw` and `--modal-max-width`, so base sheet templates hug the viewport while large-screen overrides (for example, `lg:max-w-4xl`) can still opt into broader layouts. Reach for `--modal-max-width` directly when the panel should stay anchored to the canonical modal width across breakpoints.
Compose them together when mounting new modal portals. If you need a drawer or sheet, start from these primitives and adjust spacing/positioning via utilities.
#### Modal Actions
Action rows in login, disclaimer, and upload flows now pair `.btn-ghost` and `.btn` directly. Layer layout utilities such as `w-full` or `gap-3` without re-adding custom focus rings.
```html
<footer class="modal-footer flex gap-3">
<button class="btn-ghost flex-1" type="button">Cancel</button>
<button class="btn flex-1" type="submit">Publish</button>
</footer>
```
Static modal partials (login, application, feedback, etc.) should call the helpers exported from `js/ui/components/staticModalAccessibility.js`—`prepareStaticModal`, `openStaticModal`, and `closeStaticModal`—to attach focus trapping, Escape/backdrop dismissal, and the shared `data-open`/`html.modal-open` toggles without duplicating controller logic.
### Headers
`.ds-header` replaces the legacy `header { … }` reset. Apply it to page chrome that should inherit the shared rhythm: flex alignment (`items-center`, `justify-start`) and tokenized vertical padding (`py-md`). Layer layout utilities per surface—for example, `justify-between` on the kitchen-sink demo or `mb-8` on the app shell.
Pair `.ds-header__logo` with `<img>` or inline logo wrappers to standardize sizing (`h-24`, `w-auto`, `max-w-none`) without reintroducing element selectors. Compose additional utilities (`h-16`, responsive overrides) when a view needs alternate scale.
Logo glyphs now rely on dedicated selectors inside the SVG: apply `.bv-logo__accent` to the nine-block mark, `.bv-logo__wordmark` to the lettering, and `.bv-logo__background` to any surrounding tile. These hooks read from the CSS custom properties that `.bv-logo` seeds, so themes can flip palettes purely through attributes (`data-variant`, `data-wordmark`, etc.) without reintroducing inline `fill` or `color` overrides.
### Popovers
Use `.popover` as the relative anchor for floating content. Nest `.popover__panel` for the floating surface—it’s positioned with the shared popover engine, inherits the shared focus ring, and animates with opacity/scale tokens. Toggle `[data-state="open"]` on the panel to switch between the collapsed (`opacity-0 scale-95`) and expanded (`opacity-100 scale-100`) states.
```html
<div class="popover">
<button class="btn" type="button">Open filters</button>
<div class="popover__panel" role="dialog" data-state="open">
<p class="text-sm text-muted">Fine-tune the current feed using tags and duration filters.</p>
</div>
</div>
```
Use the shared popover engine (`js/ui/overlay/popoverEngine.js`) instead of manual offsets. The helper attaches panels to the overlay root, flips placement when they collide with the viewport, and toggles `aria` + `data-popover-state` attributes as visibility changes. Width and height clamps can be sourced from design tokens (`maxWidthToken`, `maxHeightToken`) so surfaces inherit theme-specific limits without inline constants. Consult [docs/menus.md](./menus.md) for overlay-root wiring, Tailwind overlay helpers, and the manual QA surface that exercises card and modal menus. 【F:js/ui/overlay/popoverEngine.js†L252-L266】【F:js/ui/overlay/popoverEngine.js†L633-L706】【F:css/tailwind.source.css†L358-L399】
```js
import createPopover from "../overlay/popoverEngine.js";
const popover = createPopover(triggerEl, ({ container }) => {
const panel = document.createElement("div");
panel.className = "popover__panel card";
panel.dataset.state = "closed";
panel.innerHTML = "<p class=\"text-sm text-muted\">Filters</p>";
container.appendChild(panel);
return panel;
}, {
placement: "bottom-start",
maxWidthToken: "--popover-inline-safe-max",
});
await popover.open();
panel.dataset.state = "open"; // reveal animation hooks
// Later, close the surface and restore focus.
panel.dataset.state = "closed";
popover.close();
```
The render callback runs once the engine has created its portal container. Return the panel (and optional arrow) element so the helper can manage focus targets, `aria-controls`, and collision handling. Consumers remain responsible for their own visual state toggles (`data-state`) so Tailwind transitions continue to apply. Use `popover.update()` when the trigger or panel size changes, and call `popover.destroy()` during teardown to drop listeners and portals. 【F:js/ui/overlay/popoverEngine.js†L420-L583】【F:js/ui/overlay/popoverEngine.js†L888-L1014】
Recommended container pattern:
- Wrap the trigger and panel in `.popover` so the markup stays discoverable and inherits spacing tokens.
- Leave the wrapper’s overflow visible; the helper defaults to `position: fixed`, so floating surfaces escape scroll containers without getting clipped. Opt into `{ strategy: "absolute" }` if you intentionally want the panel constrained.
- When tearing down a component, call `popover.destroy()` to drop scroll/resize listeners and remove the portal container.
The motion helpers defer to the global motion tokens so Task 5 can adjust timing globally without revisiting individual components. Animations respect `prefers-reduced-motion` by collapsing to opacity-only transitions.
### Sidebar shell & controls
The navigation rail pulls its rhythm from dedicated sidebar tokens in `css/tokens.css`. The shell, panels, and footer reuse shared spacing primitives (`--space-sidebar-shell-inline`, `--space-sidebar-shell-block-start`, `--space-sidebar-shell-block-end`, `--space-sidebar-panel-gap`, `--space-sidebar-panel-padding-block`, `--space-sidebar-panel-padding-inline`, `--space-sidebar-footer-gap`) alongside component-specific radii and shadows (`--radius-sidebar-shell`, `--radius-sidebar-panel`, `--radius-sidebar-nav`, `--shadow-sidebar-shell`, `--shadow-sidebar-accent`, `--shadow-sidebar-trigger`, `--shadow-sidebar-dropup`, `--shadow-sidebar-panel`, `--shadow-sidebar-focus-ring`). These values also size fallback classes like `.sidebar-nav-link`, `.sidebar-dropup-trigger`, and `.sidebar-dropup-panel` so legacy templates stay legible. 【F:css/tokens.css†L94-L138】【F:css/tailwind.source.css†L1829-L2182】
Frosted glass treatments share a blur stack as well. Reference `--sidebar-backdrop-blur` across the shell, nav links, and footer triggers, then pull the matching saturation tokens (`--sidebar-shell-backdrop-saturation`, `--sidebar-nav-backdrop-saturation`, `--sidebar-trigger-backdrop-saturation`, `--sidebar-panel-backdrop-saturation`) or `--sidebar-panel-backdrop-blur` for popovers instead of reintroducing literal `blur()`/`saturate()` values. 【F:css/tokens.css†L70-L118】【F:css/tailwind.source.css†L2059-L2402】
Tailwind now exposes matching utilities for layouts that opt into the design system directly: use `px-sidebar-shell-inline`, `pt-sidebar-shell-block-start`, `pb-sidebar-shell-block-end`, `gap-sidebar-panel-gap`, and `rounded-sidebar-shell`/`rounded-sidebar-nav` for structure, then `shadow-sidebar-shell`, `shadow-sidebar-accent`, or `shadow-sidebar-dropup` to match the bespoke glow states. Combine them with existing color utilities to compose new sidebar sections without reintroducing raw measurements. 【F:tailwind.config.cjs†L115-L155】
Iconography and motion helpers rely on fresh tokens as well: `--icon-size-sm`, `--icon-size-md`, and `--icon-size-lg` standardise glyph sizing across nav links, collapse toggles, and dropup triggers, while `--space-sidebar-nav-translate` powers the translate offsets used when collapsing the rail. Thin overlays inherit `--scrollbar-thin-width` so modal and sidebar scroll containers feel identical.
### Menus
`.menu` normalises list-based command menus with consistent padding. Pair it with `.menu__heading` for optional section labels, `.menu__separator` for dividers, and `.menu__item` for actionable rows. Each item supports hover, active (`data-state="active"`), and critical (`data-variant="critical"`) palettes plus disabled fallbacks through either `disabled` or `aria-disabled="true"`.
```html
<div class="popover">
<button class="btn-ghost" type="button">Command palette</button>
<div class="popover__panel" role="menu" data-state="open">
<div class="menu" role="none">
<p class="menu__heading" role="presentation">Navigation</p>
<button class="menu__item" role="menuitem">Search videos</button>
<button class="menu__item" role="menuitem" data-state="active">Go live dashboard</button>
<div class="menu__separator" role="separator"></div>
<button class="menu__item" role="menuitem" data-variant="critical">Delete current draft</button>
<button class="menu__item" role="menuitem" disabled>Switch profile (pro only)</button>
</div>
</div>
</div>
```
The helpers rely on layout utilities (`flex`, `gap-*`) so you can safely inject icons, shortcuts, or nested badges without restyling the base tokens.
### Zap Popover
Compose `.popover` with a zap-specific panel variant when you need to surface the tipping form inline. Toggle `data-state="open"` on the `.popover__panel` and remove its `hidden` attribute when the dialog is visible. The `data-variant="zap"` hook reuses the frosted-glass styling defined in `css/tailwind.source.css`.
```html
<div class="popover">
<button class="btn-ghost h-10 w-10 rounded-full p-0" type="button" aria-haspopup="dialog" aria-expanded="false">
<img src="assets/svg/lightning-bolt.svg" alt="Zap" class="h-5 w-5" />
</button>
<div
class="popover__panel card w-72 max-w-popover-safe space-y-4"
role="dialog"
aria-hidden="true"
data-variant="zap"
data-state="closed"
hidden
>
<div class="flex items-center justify-between gap-4">
<h3 class="text-xs font-semibold uppercase tracking-wide text-muted">Send a zap</h3>
<button class="btn-ghost h-8 w-8 rounded-full p-0" type="button" aria-label="Close zap dialog">✕</button>
</div>
<form class="space-y-4">
<label class="block text-xs font-semibold uppercase tracking-wide text-muted" for="zapAmountExample">
Zap amount (sats)
</label>
<input id="zapAmountExample" class="input" type="number" min="1" step="1" inputmode="numeric" />
<button class="btn w-full" type="submit">Send</button>
</form>
</div>
</div>
```
### Grid Stack
`.grid-stack` offers gap tokens for dense layouts. Add `data-variant="cards"` for responsive card grids (`grid-cols-1` → `sm:grid-cols-2` → `xl:grid-cols-3`). Switch to `data-orientation="vertical"` for stacked flex columns that reuse the same spacing tokens.
### Layout Helpers
- `.bv-stack` creates a vertical flex stack with the default large gap (`var(--space-lg)`). Use it for page sections such as the watch-history summary blocks.
- `.bv-stack--tight` reduces the gap to `var(--space-md)` while inheriting the `.bv-stack` flex behaviour. Combine the modifier when stacking smaller card fragments or form controls inside profile templates.
- `.bv-grid-video` centralises the responsive video grid (`repeat(auto-fill, minmax(20rem, 1fr))`) with the standard gutter (`gap-xl`) and vertical padding (`py-lg`). Apply it to feed, subscriptions, and channel views instead of repeating inline grid rules.
## Legacy Compatibility Shims
During migration we keep existing selectors alive by layering `@apply` calls in `css/tailwind.source.css`. Notable shims:
- `#profileModal .profile-switcher*` → `.card`, `.badge`, `.btn-ghost`
> **2025-10-11:** The `.video-card` shim family was removed. Templates must emit `.card` elements with the appropriate `data-state` and `data-alert` attributes instead of relying on the legacy class names.
We continue to expose aliases such as `.profile-switcher` and nested variations (for example, `.profile-switcher__item`) so existing profile and admin controllers stay functional while downstream templates finish their migration. These shims live under the "Legacy component compatibility" section and will be removed once templates switch to the new primitives.
> **Modal primitives:** Always mount `.bv-modal`, `.bv-modal-backdrop`, and `.bv-modal__panel` together. Controllers expect this trio, so there is no compatibility layer for alternate selectors.
**Shim removal checklist (Task DS-742):**
- [ ] Confirm no production templates emit `.profile-switcher*` selectors (owners: @ui-templates, @design-systems).
- [ ] Verify the torrent beacon renders `.card`, `.btn`, `.btn-ghost`, and `.input` primitives with no fallback selectors (owner: @torrent-beacon).
- [ ] Verify staging renders with `data-ds="new"` across surfaces and run kitchen-sink visual snapshots to ensure no regressions.
- [ ] Delete residual `@apply` rules under "Legacy component compatibility" in `css/tailwind.source.css`.
- [ ] Announce the removal in release notes and update downstream embed documentation.
**Owners:** Design Systems Guild (@design-systems) with support from the Templates crew (@ui-templates) and the torrent beacon maintainers (@torrent-beacon).
**Target removal release:** 2025.04 mainline (aligns with the shim removal milestone tracked in DS-742). Audit usage after the Q1 2025 UI refresh and delete remaining shims no later than the April 2025 deployment window. At that point the compatibility layer will be removed once dependent feature flags confirm that no legacy selectors remain in production HTML.
### Selector migration table
| Legacy selector | Replacement primitive(s) | Beacon / app notes | Feature-flag strategy |
| --- | --- | --- | --- |
| `.video-card`, `.video-card__meta`, `.video-card--loading` | `.card` with `data-state` + utility classes | Removed in web app; ensure beacon dashboards render `.card` with skeleton states instead of `.video-card--loading`. | Deploy directly and validate in staging before rolling to beacon.
| `.profile-switcher`, `.profile-switcher__item` | `.card` rows + `.btn-ghost` toggles | Keep shim until profile modal Reactors deploy; beacon does not consume this selector. | Roll out once QA signs off; no flag gating remains.
| `.button`, `.button-danger` (Skeleton) | `.btn`, `.btn-ghost[data-variant="critical"]` | Critical for torrent beacon—replace the Angular templates before removing Skeleton CDN. | Coordinate a staged deploy; the design system is always on so fallbacks must ship in a separate release if required.
| `.u-full-width`, `.input-text` | `.input`, `.select`, `.form-control` | Applies to upload forms and beacon filter controls; also update tests that target legacy classes. | Update markup globally; controllers now always run in the design system mode.
| `.modal-container`, `.modal-content` | `.bv-modal`, `.bv-modal-backdrop`, `.bv-modal__panel` | Retired selectors—emit the primitives exactly so accessibility, animation, and controller hooks stay wired. | N/A — unified primitives only. |
### Migration Notes (Q4 2024)
- Modal templates across the app now mount `.bv-modal`, `.bv-modal-backdrop`, and `.bv-modal__panel` primitives directly. Controllers such as `ProfileModalController` and the upload modal orchestrator were updated to expect these primitives, so any new modal should follow the same structure for consistent accessibility and animation hooks.
- The compatibility shim keeps the `.profile-switcher` selector family alive temporarily so third-party embeds and cached HTML fragments continue to render while operators redeploy updated templates. Keep the aliases until the April 2025 removal window above, then delete the shim and update any last callers to the new `.card`/`.btn-ghost` structure.
### Overlay layering scale
Overlay stacks now use semantic z-index tokens. Reach for these utilities instead of raw numeric values so floating surfaces remain predictable across the app:
| Token | Utility class | Primary usage |
| --- | --- | --- |
| `--z-overlay-nav` | `z-overlay-nav` | Mobile navigation toggles, sticky modal nav bars |
| `--z-overlay-floating` | `z-overlay-floating` | Popovers, floating status overlays, async progress shells |
| `--z-overlay-toast` | `z-overlay-toast` | Toast managers and transient notifications |
| `--z-modal-root` / `--z-modal-overlay` / `--z-modal-content` | `z-[var(--z-modal-root)]` primitives | Modal scaffolding (`.bv-modal`, `.bv-modal-backdrop`, `.bv-modal__panel`) |
Backdrops share a common helper: `.ds-overlay-backdrop` applies the fixed positioning, tone, and blur defined by `--overlay-backdrop-color` / `--overlay-backdrop-blur`. Pair it with the appropriate `z-overlay-*` utility when you need to tune stacking above or below other surfaces.
## Beacon client migration
The torrent beacon now shares the Tailwind bundle used across bitvid. The standalone `torrent/beacon.html` drops Skeleton/FoA CDNs in favour of `css/tailwind.generated.css`, adopts the `.card`, `.btn`, `.btn-ghost`, and `.input` primitives, and relies on `.bv-stack` for spacing inside Angular partials. Inline SVG icons replace FontAwesome so controls stay lightweight while inheriting tokenised focus and hover states.
Angular templates under `torrent/views/` should follow the refreshed structure:
- Wrap primary sections in `.card` and layer Tailwind utilities for layout (`grid`, `gap-xl`, `overflow-auto`).
- Use `.input`, `.select`, `.btn`, and `.btn-ghost` (with `data-variant="critical"` for destructive actions) instead of legacy `.u-full-width`, `.button`, or `.button-danger` classes.
- Prefer token-backed utilities for spacing and typography (`px-6`, `py-6`, `text-muted`) over ad-hoc CSS.
- Reuse the fixed overlay spinner pattern (`ds-overlay-backdrop z-overlay-floating` plus an `animate-spin text-info-strong` SVG) for async states so overlays stay consistent with the new palette.
When adding new torrent affordances, mirror the existing markup: inline 24×24 SVG strokes at 2px, `.focus-ring` on interactive anchors, and `.bv-stack` for vertical rhythm. This keeps the beacon aligned with the design system without reintroducing the deprecated Skeleton grid.
## When to Mix Utilities
Use the primitives for foundation and lean on Tailwind utilities for:
- Layout (`flex`, `grid`, `gap-*`, `justify-*`)
- Sizing (`w-*`, `h-*`, `min-w-*`)
- Typography tweaks (`text-left`, `font-light`)
- State variations not yet encoded as tokens (e.g., success/neutral buttons)
Avoid re-declaring token values manually. If a new pattern repeats, promote it into a primitive or request new tokens to keep styling consistent.
## Visual snapshot review workflow
Continuous integration captures Playwright-powered screenshots of `docs/kitchen-sink.html` (`tests/visual/kitchen-sink.spec.ts`) on every pull request. When a snapshot job fails:
1. Read the diff produced by Playwright (attached as an artifact) and compare it with your local preview. The harness highlights per-pixel changes so reviewers can distinguish deliberate theme updates from regressions.
2. Reproduce locally with `npm run test:visual -- --update-snapshots` to refresh the expected images after verifying the change in the kitchen sink.
3. Document the intent in the PR description (include before/after screenshots or reference the kitchen-sink toggle state) so reviewers and QA know the snapshot change is expected.
The design system doc is the single source of truth for reviewing these failures—if a diff contradicts the guidance above, treat it as a regression, request updates, and block the merge until the visual snapshot stabilises.
+1 -1
View File
@@ -1,6 +1,6 @@
# Feed Engine Architecture
The feed engine is a lightweight pipeline for composing Bitvid feeds from one
The feed engine is a lightweight pipeline for composing bitvid feeds from one
or more data sources. It lives in `js/feedEngine/` and exposes factories for
sources, stages, sorters, and the engine itself. The goal is to make it easy to
register new feeds today while leaving room for future "open algorithm" work
+735 -147
View File
@@ -3,175 +3,763 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Bitvid Tailwind Kitchen Sink</title>
<title>bitvid Design System Kitchen Sink</title>
<link rel="stylesheet" href="../css/tailwind.generated.css" />
</head>
<body class="bg-page font-sans text-text-strong">
<div class="min-h-screen bg-page-alt">
<header class="bg-surface text-text shadow-modal">
<body class="bg-page font-sans text-text-strong" data-ds="new">
<div class="min-h-screen bg-page-alt" data-ds="new">
<header class="ds-header bg-surface text-text shadow-modal">
<div
class="mx-auto flex max-w-6xl items-center justify-between px-md py-sm"
class="mx-auto flex max-w-6xl items-center justify-between gap-sm px-md py-sm"
>
<h1 class="text-2xl font-semibold">Bitvid UI Kitchen Sink</h1>
<button
class="rounded-lg bg-primary px-sm py-2 text-white shadow-md transition duration-fast ease-out hover:bg-secondary focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
>
Primary Action
</button>
<h1 class="text-2xl font-semibold">bitvid UI Kitchen Sink</h1>
<div class="flex items-center gap-sm">
<label class="sr-only" for="themeSwitcher">Theme</label>
<select id="themeSwitcher" class="select h-10" aria-label="Select theme">
<option value="default">Default (Dark)</option>
<option value="light">Light</option>
<option value="contrast">High Contrast (Preview)</option>
</select>
<button class="btn">Primary Action</button>
</div>
</div>
</header>
<main class="mx-auto flex max-w-6xl flex-col gap-lg px-md py-lg">
<main class="mx-auto max-w-6xl space-y-xl px-md py-xl">
<section class="space-y-md">
<h2 class="text-xl font-semibold text-text-strong">Buttons</h2>
<div class="flex flex-wrap gap-sm">
<button
class="rounded-md bg-primary px-sm py-2 text-white shadow-md transition duration-fast ease-out hover:bg-secondary"
>
Primary
</button>
<button
class="rounded-md bg-info px-sm py-2 text-surface shadow-sm transition duration-fast ease-out hover:bg-info-strong"
>
Informational
</button>
<button
class="rounded-md bg-critical px-sm py-2 text-white shadow-sm transition duration-fast ease-out hover:bg-critical-strong"
>
Destructive
</button>
<button
class="rounded-md border border-border-subtle px-sm py-2 text-text-muted shadow-none transition duration-fast ease-out hover:bg-panel-hover hover:text-text"
>
Ghost
</button>
<div class="space-y-xs">
<h2 class="text-xl font-semibold text-text-strong">Buttons</h2>
<p class="text-sm text-muted">
Each block shows the default, simulated hover, focus, and disabled
states.
</p>
</div>
</section>
<section class="space-y-md">
<h2 class="text-xl font-semibold text-text-strong">Cards</h2>
<div class="grid gap-md md:grid-cols-3">
<article class="rounded-lg bg-card p-md text-text shadow-lg">
<h3 class="text-lg font-semibold">Default Card</h3>
<p class="mt-xs text-text-muted">
Uses
<code class="font-mono text-sm text-info">bg-card</code> and
tokenized shadows.
</p>
<button
class="mt-sm rounded-md bg-info px-sm py-2 text-surface shadow-sm transition duration-base ease-in-out hover:bg-info-strong"
>
Watch Now
</button>
</article>
<article
class="rounded-lg bg-card-private p-md text-text shadow-lg"
>
<h3 class="text-lg font-semibold">Private Card</h3>
<p class="mt-xs text-text-muted">
Emphasizes restricted content with
<code class="font-mono text-sm text-info">bg-card-private</code
>.
</p>
</article>
<article
class="rounded-lg bg-card-critical p-md text-text shadow-lg"
>
<h3 class="text-lg font-semibold">Critical Card</h3>
<p class="mt-xs text-text-muted">
Use for outage or abuse states.
</p>
</article>
</div>
</section>
<section class="space-y-md">
<h2 class="text-xl font-semibold text-text-strong">Form Inputs</h2>
<form class="grid gap-md md:grid-cols-2">
<label class="flex flex-col gap-xs">
<span class="text-sm font-medium text-text-muted"
>Display Name</span
>
<input
class="rounded-md border border-border-subtle bg-white px-sm py-xs text-text-strong shadow-sm focus:border-info focus:ring-2 focus:ring-info-strong"
type="text"
placeholder="Satoshi Stack"
/>
</label>
<label class="flex flex-col gap-xs">
<span class="text-sm font-medium text-text-muted">Video URL</span>
<input
class="rounded-md border border-border-subtle bg-white px-sm py-xs text-text-strong shadow-sm focus:border-info focus:ring-2 focus:ring-info-strong"
type="url"
placeholder="https://example.com/video.mp4"
/>
</label>
<label class="flex flex-col gap-xs md:col-span-2">
<span class="text-sm font-medium text-text-muted"
>Description</span
>
<textarea
class="rounded-md border border-border-subtle bg-white px-sm py-xs text-text-strong shadow-sm focus:border-info focus:ring-2 focus:ring-info-strong"
rows="3"
placeholder="Describe what viewers should expect..."
></textarea>
</label>
</form>
</section>
<section class="space-y-md">
<h2 class="text-xl font-semibold text-text-strong">Modal Preview</h2>
<div
class="relative overflow-hidden rounded-xl bg-overlay p-lg text-text shadow-modal"
>
<div class="grid gap-lg lg:grid-cols-2">
<div class="space-y-sm">
<h3 class="text-lg font-semibold">Upload Video</h3>
<p class="text-text-muted">
Motion tokens control the fade via
<code class="font-mono text-sm text-info">duration-base</code>
and
<code class="font-mono text-sm text-info">ease-out</code>
utilities.
</p>
<h3 class="text-lg font-semibold">Primary (.btn)</h3>
<div class="grid gap-sm sm:grid-cols-2 xl:grid-cols-4">
<div class="space-y-xs">
<p class="text-xs uppercase text-muted">Default</p>
<button class="btn">Default</button>
</div>
<div class="space-y-xs">
<p class="text-xs uppercase text-muted">Hover (simulated)</p>
<button class="btn bg-info-strong shadow-lg">Hover</button>
</div>
<div class="space-y-xs">
<p class="text-xs uppercase text-muted">Focus (simulated)</p>
<button
class="btn bg-info-pressed ring-2 ring-info ring-opacity-70 ring-offset-2 ring-offset-surface"
>
Focus
</button>
</div>
<div class="space-y-xs">
<p class="text-xs uppercase text-muted">Disabled</p>
<button class="btn" disabled>Disabled</button>
</div>
</div>
</div>
<div class="mt-md flex flex-col gap-sm">
<input
class="rounded-md border border-border-subtle bg-white px-sm py-xs text-text-strong focus:border-info focus:ring-2 focus:ring-info"
type="text"
placeholder="Magnet URI"
/>
<div class="flex justify-end gap-sm">
<button
class="rounded-md border border-border px-sm py-2 text-text-muted transition duration-fast ease-out hover:text-text"
>
Cancel
</button>
<button
class="rounded-md bg-primary px-sm py-2 text-white shadow-md transition duration-fast ease-out hover:bg-secondary"
>
Publish
</button>
<div class="space-y-sm">
<h3 class="text-lg font-semibold">Ghost (.btn-ghost)</h3>
<div class="grid gap-sm sm:grid-cols-2 xl:grid-cols-4">
<div class="space-y-xs">
<p class="text-xs uppercase text-muted">Default</p>
<button class="btn-ghost">Default</button>
</div>
<div class="space-y-xs">
<p class="text-xs uppercase text-muted">Hover (simulated)</p>
<button class="btn-ghost bg-panel-hover text-text">
Hover
</button>
</div>
<div class="space-y-xs">
<p class="text-xs uppercase text-muted">Focus (simulated)</p>
<button
class="btn-ghost bg-panel-hover ring-2 ring-info ring-opacity-70 ring-offset-2 ring-offset-surface"
>
Focus
</button>
</div>
<div class="space-y-xs">
<p class="text-xs uppercase text-muted">Disabled</p>
<button class="btn-ghost" disabled>Disabled</button>
</div>
</div>
</div>
</div>
</section>
<section class="space-y-md">
<h2 class="text-xl font-semibold text-text-strong">
Typography Scale
</h2>
<div class="space-y-xs">
<p class="text-2xl font-semibold">Display / 2xl</p>
<p class="text-xl font-semibold">Headline / xl</p>
<p class="text-lg font-medium">Title / lg</p>
<p class="text-base">Body / base</p>
<p class="text-sm text-text-muted">Caption / sm</p>
<p class="text-xs uppercase tracking-wide text-text-muted">
Meta / xs
<h2 class="text-xl font-semibold text-text-strong">Badges</h2>
<p class="text-sm text-muted">
Use `data-variant` for semantic colorways.
</p>
</div>
<div class="flex flex-wrap items-center gap-sm">
<span class="badge">Default</span>
<span class="badge" data-variant="info">Info</span>
<span class="badge" data-variant="critical">Critical</span>
<span class="badge" data-variant="neutral">Neutral</span>
</div>
</section>
<section class="space-y-md">
<div class="space-y-xs">
<h2 class="text-xl font-semibold text-text-strong">Cards</h2>
<p class="text-sm text-muted">
Cards inherit focus-ring behaviour and support semantic states.
</p>
</div>
<div class="grid-stack" data-variant="cards">
<article class="card p-md">
<h3 class="text-lg font-semibold">Default Card</h3>
<p class="mt-xs text-muted">
Combine with layout utilities like `p-md` or `gap-sm` for
content spacing.
</p>
<button class="btn mt-sm">Watch</button>
</article>
<article
class="card -translate-y-1 bg-card p-md shadow-lg transition-none"
>
<h3 class="text-lg font-semibold">Hover (simulated)</h3>
<p class="mt-xs text-muted">Applies the hover shadow and lift.</p>
</article>
<article
class="card bg-card p-md ring-2 ring-info ring-opacity-70 ring-offset-2 ring-offset-surface"
>
<h3 class="text-lg font-semibold">Focus (simulated)</h3>
<p class="mt-xs text-muted">Rings appear on `focus-visible`.</p>
</article>
<article class="card p-md" data-state="disabled">
<h3 class="text-lg font-semibold">Disabled</h3>
<p class="mt-xs text-muted">
Opacity drops and the shadow is removed.
</p>
</article>
</div>
</section>
<section class="space-y-md">
<div class="space-y-xs">
<h2 class="text-xl font-semibold text-text-strong">
Layout Helpers
</h2>
<p class="text-sm text-muted">
`.bv-stack` and `.bv-grid-video` consolidate our common feed and
profile layouts.
</p>
</div>
<div class="grid gap-lg lg:grid-cols-2">
<div class="space-y-sm">
<h3 class="text-lg font-semibold">Stack (.bv-stack)</h3>
<div class="bv-stack">
<article class="card p-md">
<h4 class="text-base font-semibold">Default spacing</h4>
<p class="text-sm text-muted">
Use `.bv-stack` for page-level groupings such as the watch
history header and banners.
</p>
</article>
<article class="card p-md">
<h4 class="text-base font-semibold">Nested card</h4>
<p class="text-sm text-muted">
The helper keeps consistent gaps without repeating
`space-y-*` utilities.
</p>
</article>
</div>
<div class="bv-stack bv-stack--tight">
<article class="card p-md">
<h4 class="text-base font-semibold">Tight stack</h4>
<p class="text-sm text-muted">
Apply `.bv-stack--tight` when stacking compact forms or
metadata blocks in profile views.
</p>
</article>
<article class="card p-md">
<h4 class="text-base font-semibold">Form footer</h4>
<p class="text-sm text-muted">
The modifier inherits flex behaviour from `.bv-stack` while
reducing the gap to `var(--space-md)`.
</p>
</article>
</div>
</div>
<div class="space-y-sm">
<h3 class="text-lg font-semibold">Video Grid (.bv-grid-video)</h3>
<div class="bv-grid-video">
<article class="card p-md">
<h4 class="text-base font-semibold">Feed Tile</h4>
<p class="text-sm text-muted">
Auto-fills with a 20rem minimum column size on desktop.
</p>
</article>
<article class="card p-md">
<h4 class="text-base font-semibold">Subscription Tile</h4>
<p class="text-sm text-muted">
The grid collapses to a single column below 40rem.
</p>
</article>
<article class="card p-md">
<h4 class="text-base font-semibold">Channel Tile</h4>
<p class="text-sm text-muted">
Reuse the same helper instead of duplicating inline grid
declarations.
</p>
</article>
</div>
</div>
</div>
</section>
<section class="space-y-md">
<div class="space-y-xs">
<h2 class="text-xl font-semibold text-text-strong">Form Inputs</h2>
<p class="text-sm text-muted">
`.input` works for both `&lt;input&gt;` and `&lt;textarea&gt;`
elements.
</p>
</div>
<div class="grid gap-md md:grid-cols-2">
<div class="space-y-xs">
<label
class="text-sm font-medium text-muted"
for="demo-input-default"
>Default</label
>
<input
id="demo-input-default"
class="input"
type="text"
placeholder="Display name"
/>
</div>
<div class="space-y-xs">
<label
class="text-sm font-medium text-muted"
for="demo-input-hover"
>Hover (simulated)</label
>
<input
id="demo-input-hover"
class="input border-border"
type="text"
value="Hovering"
/>
</div>
<div class="space-y-xs">
<label
class="text-sm font-medium text-muted"
for="demo-input-focus"
>Focus (simulated)</label
>
<input
id="demo-input-focus"
class="input border-info-strong ring-2 ring-info ring-opacity-70 ring-offset-2 ring-offset-surface"
type="text"
value="In focus"
/>
</div>
<div class="space-y-xs">
<label
class="text-sm font-medium text-muted"
for="demo-input-disabled"
>Disabled</label
>
<input
id="demo-input-disabled"
class="input"
type="text"
value="Read only"
disabled
/>
</div>
<div class="space-y-xs md:col-span-2">
<label class="text-sm font-medium text-muted" for="demo-textarea"
>Textarea</label
>
<textarea
id="demo-textarea"
class="input"
rows="3"
placeholder="Describe the video..."
></textarea>
</div>
</div>
</section>
<section class="space-y-md">
<div class="space-y-xs">
<h2 class="text-xl font-semibold text-text-strong">
Focus Ring Helper
</h2>
<p class="text-sm text-muted">
Add `.focus-ring` to custom interactives when you opt out of
`.btn` or other primitives.
</p>
</div>
<div class="flex flex-wrap gap-sm">
<a
href="#"
class="focus-ring inline-flex items-center gap-xs rounded-md border border-border bg-panel px-sm py-xs text-text no-underline"
>
Focusable Link
</a>
<button
class="focus-ring inline-flex items-center gap-xs rounded-md border border-border px-sm py-xs"
>
Plain Button
</button>
</div>
</section>
<section class="space-y-md">
<div class="space-y-xs">
<h2 class="text-xl font-semibold text-text-strong">
Modal Surface
</h2>
<p class="text-sm text-muted">
`.bv-modal` anchors the overlay, `.bv-modal-backdrop` dims the
page, and `.bv-modal__panel` styles content.
</p>
</div>
<div
class="relative isolate overflow-hidden rounded-xl bg-surface-alt p-lg text-text shadow-inner"
>
<div class="relative h-full w-full">
<div class="bv-modal absolute inset-0">
<div class="bv-modal-backdrop"></div>
<div class="bv-modal__panel space-y-6">
<header class="space-y-2">
<h3 class="text-2xl font-semibold tracking-tight text-text">
Upload Video
</h3>
<p class="text-sm text-text">
Use `.input`, `.btn-ghost`, and `.modal-primary`
primitives for consistent modal actions.
</p>
</header>
<input class="input" type="text" placeholder="Magnet URI" />
<div class="flex flex-col gap-3 pt-2 sm:flex-row sm:justify-end">
<button class="btn-ghost w-full sm:w-auto">Cancel</button>
<button class="modal-primary sm:w-auto">
Publish
</button>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="space-y-md">
<div class="space-y-xs">
<h2 class="text-xl font-semibold text-text-strong">
Profile &amp; Edit Modal Shells
</h2>
<p class="text-sm text-muted">
Both shells share the `.bv-modal` overlay, `.bv-modal__panel`
surface, and tokenized primitives (`.card`, `.btn`, `.btn-ghost`,
`.badge`, `.grid-stack`, `.focus-ring`). The templates below match
the production markup so controller behaviour and documentation
stay aligned.
</p>
</div>
<div class="grid gap-lg lg:grid-cols-2">
<div class="space-y-sm">
<h3 class="text-lg font-semibold">Profile Modal</h3>
<p class="text-sm text-muted">
Static scaffold that mirrors `components/profile-modal.html`.
Tab buttons keep their `aria-controls`/`data-state` attributes
so designers can map the layout to controller behaviour.
</p>
<div
class="relative isolate overflow-hidden rounded-xl bg-surface-alt p-lg text-text shadow-inner"
>
<div class="relative h-full w-full">
<div
id="profileModal"
class="bv-modal absolute inset-0 items-start"
>
<div class="bv-modal-backdrop"></div>
<div
class="bv-modal__panel flex max-h-modal-embed flex-col gap-lg"
role="dialog"
aria-labelledby="profileModalTitle"
>
<header
class="card flex items-center justify-between gap-sm p-md"
>
<div>
<p class="text-xs uppercase tracking-wide text-muted">
Modal header
</p>
<h4
id="profileModalTitle"
class="text-lg font-semibold"
>
Active profile
</h4>
</div>
<button
class="focus-ring inline-flex h-10 w-10 items-center justify-center rounded-full"
type="button"
aria-label="Close profile modal"
>
&times;
</button>
</header>
<div class="flex flex-1 flex-col gap-lg lg:flex-row">
<nav
class="grid gap-xs sm:grid-cols-3 lg:flex lg:w-56 lg:flex-col"
aria-label="Profile sections"
>
<button
class="btn-ghost focus-ring justify-start"
type="button"
data-state="active"
aria-controls="profilePaneAccount"
>
Account
</button>
<button
class="btn-ghost focus-ring justify-start"
type="button"
aria-controls="profilePaneRelays"
>
Relays
</button>
<button
class="btn-ghost focus-ring justify-start"
type="button"
aria-controls="profilePaneBlocked"
>
Blocked users
</button>
</nav>
<div
class="grid-stack flex-1 gap-md"
data-variant="cards"
>
<article
id="profilePaneAccount"
class="card space-y-sm p-md"
data-profile-pane
>
<div
class="flex items-start justify-between gap-sm"
>
<div>
<p
class="text-xs uppercase tracking-wide text-muted"
>
Current identity
</p>
<h5 class="text-base font-semibold">
npub1example...
</h5>
</div>
<span class="badge" data-variant="info"
>Active</span
>
</div>
<p class="text-sm text-muted">
`.grid-stack` arranges saved identities with
responsive gutters. Cards inherit `.focus-ring`
styles when they become actionable.
</p>
<footer class="flex flex-wrap gap-sm">
<button class="btn">Switch</button>
<button class="btn-ghost">Manage keys</button>
</footer>
</article>
<article
class="card space-y-sm p-md"
data-profile-pane
>
<h5 class="text-base font-semibold">
Relay presets
</h5>
<p class="text-sm text-muted">
Use badges to indicate status (e.g.
<span class="badge" data-variant="neutral"
>New</span
>) while keeping content spacing consistent with
the production modal.
</p>
<ul class="space-y-xs text-sm text-muted">
<li data-relay-row>relay.example.com</li>
<li data-relay-row>wss://nostr.tools</li>
<li data-relay-row>wss://bitvid.relays</li>
</ul>
</article>
</div>
</div>
<footer
class="card flex flex-wrap items-center justify-between gap-sm p-md"
>
<p class="text-xs text-muted">
Footer keeps shared spacing tokens while exposing
legacy hooks like `data-profile-action` for scripts if
needed.
</p>
<div class="flex gap-sm">
<button
class="btn-ghost"
data-profile-action="cancel"
>
Cancel
</button>
<button class="btn" data-profile-action="save">
Save changes
</button>
</div>
</footer>
</div>
</div>
</div>
</div>
</div>
<div class="space-y-sm">
<h3 class="text-lg font-semibold">Edit Video Modal</h3>
<p class="text-sm text-muted">
Layout reflects `components/edit-video-modal.html` with the
`.bv-modal__panel` shell controlling spacing and typography. Use
the modal primitives directly when composing new flows.
</p>
<div
class="relative isolate overflow-hidden rounded-xl bg-surface-alt p-lg text-text shadow-inner"
>
<div class="relative h-full w-full">
<div
id="editVideoModal"
class="bv-modal absolute inset-0 items-start"
role="dialog"
aria-labelledby="editVideoModalTitle"
>
<div class="bv-modal-backdrop"></div>
<div
class="bv-modal__panel flex max-h-modal-embed flex-col gap-lg"
data-modal="edit"
>
<header
class="card flex items-center justify-between gap-sm p-md"
>
<div class="space-y-xs">
<p class="text-xs uppercase tracking-wide text-muted">
Edit metadata
</p>
<h4
id="editVideoModalTitle"
class="text-lg font-semibold"
>
Update video note
</h4>
</div>
<button
class="focus-ring inline-flex h-10 w-10 items-center justify-center rounded-full"
type="button"
aria-label="Close edit modal"
>
&times;
</button>
</header>
<div class="space-y-lg px-md">
<p class="text-sm text-muted">
Form rows reuse `.card` groupings and `.btn-ghost`
action chips. Locked fields can keep
`data-edit-target` attributes to stay compatible with
controller toggles.
</p>
<div class="card space-y-md p-md">
<div class="space-y-xs">
<label
class="text-sm font-medium text-text"
for="editTitleDemo"
>
Title
</label>
<div class="flex flex-wrap gap-sm sm:flex-nowrap">
<input
id="editTitleDemo"
class="input flex-1"
type="text"
placeholder="Bitvid Weekly Update"
/>
<button
class="btn-ghost focus-ring"
type="button"
data-edit-target="title"
>
Edit field
</button>
</div>
</div>
<div
class="grid gap-md md:grid-cols-2"
data-nip71-grid
>
<div class="space-y-xs">
<label
class="text-sm font-medium text-text"
for="editKindDemo"
>
NIP-71 kind
</label>
<div
class="card space-y-xs p-sm"
data-nip71-fieldset
>
<label class="flex items-center gap-xs text-sm">
<input
class="focus-ring"
type="radio"
name="editKindDemo"
checked
/>
Kind 21 — Full video
</label>
<label class="flex items-center gap-xs text-sm">
<input
class="focus-ring"
type="radio"
name="editKindDemo"
/>
Kind 22 — Short
</label>
</div>
</div>
<div class="space-y-xs">
<label
class="text-sm font-medium text-text"
for="editPublishDemo"
>
Published at
</label>
<input
id="editPublishDemo"
class="input"
type="datetime-local"
value="2024-05-01T15:00"
/>
<p class="text-xs text-muted">
Shared input tokens handle spacing; legacy
scripts can read the timestamp via
`data-nip71-input` if needed.
</p>
</div>
</div>
</div>
</div>
<footer
class="card flex flex-wrap items-center justify-between gap-sm p-md"
>
<p class="text-xs text-muted">
Footer actions mix `.btn` variants while referencing
`data-submit-state` for controller hand-offs.
</p>
<div class="flex gap-sm">
<button class="btn-ghost" data-submit-state="idle">
Cancel
</button>
<button class="btn" data-submit-state="pending">
Save draft
</button>
</div>
</footer>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="space-y-md">
<div class="space-y-xs">
<h2 class="text-xl font-semibold text-text-strong">Grid Stack</h2>
<p class="text-sm text-muted">
Use `data-variant="cards"` for responsive columns or
`data-orientation="vertical"` for flex stacks.
</p>
</div>
<div class="grid gap-lg lg:grid-cols-2">
<div class="space-y-sm">
<h3 class="text-lg font-semibold">Card Grid</h3>
<div class="grid-stack" data-variant="cards">
<div class="card p-md">
<p class="text-sm text-muted">Card A</p>
</div>
<div class="card p-md">
<p class="text-sm text-muted">Card B</p>
</div>
<div class="card p-md">
<p class="text-sm text-muted">Card C</p>
</div>
</div>
</div>
<div class="space-y-sm">
<h3 class="text-lg font-semibold">Vertical Stack</h3>
<div class="grid-stack" data-orientation="vertical">
<div class="card p-sm">
<p class="text-sm text-muted">Row 1</p>
</div>
<div class="card p-sm">
<p class="text-sm text-muted">Row 2</p>
</div>
<div class="card p-sm" data-state="disabled">
<p class="text-sm text-muted">Row 3 (disabled)</p>
</div>
</div>
</div>
</div>
</section>
</main>
</div>
<script>
(() => {
const storageKey = "bitvid-docs-theme";
const root = document.documentElement;
const switcher = document.getElementById("themeSwitcher");
const applyTheme = (value) => {
if (!value || value === "default") {
delete root.dataset.theme;
} else {
root.dataset.theme = value;
}
};
let savedTheme;
try {
savedTheme = window.localStorage.getItem(storageKey) || undefined;
} catch (error) {
savedTheme = undefined;
}
applyTheme(savedTheme);
if (switcher) {
switcher.value = savedTheme ?? "default";
switcher.addEventListener("change", (event) => {
const { value } = event.target;
applyTheme(value);
try {
if (!value || value === "default") {
window.localStorage.removeItem(storageKey);
} else {
window.localStorage.setItem(storageKey, value);
}
} catch (error) {
/* Ignore storage failures (private mode, etc.) */
}
});
}
})();
</script>
<div id="uiOverlay" data-overlay-root aria-hidden="true"></div>
</body>
</html>
+68
View File
@@ -0,0 +1,68 @@
# Logging and Dev-Mode Guidance
This document explains how bitvid routes browser logs, how inline scripts can
re-use the shared logger, and what operators must do with the dev-mode flag when
promoting builds.
## Picking the right channel
bitvid ships a frozen logger at `js/utils/logger.js` with two channels:
- `logger.dev`: Emits only when `IS_DEV_MODE` is truthy. Use this channel for
verbose traces, performance probes, and feature flag experimentation that
should disappear from production consoles. The `logger.dev.debug` helper is
available for especially noisy traces that you want to keep isolated to local
development sessions.
- `logger.user`: Always available and automatically prefixes output with
`[bitvid]`. Use this channel for operator-facing warnings and errors that help
diagnose issues in production.
When adding new messages, classify them by the audience:
| Scenario | Recommended channel | Notes |
| --- | --- | --- |
| Debugging a new feature locally | `logger.dev.debug`/`logger.dev.info` | Remove or downgrade before launching if they no longer serve a purpose. |
| Non-fatal recoverable issues users should investigate | `logger.user.warn` | Provide actionable context so operators know what failed. |
| Fatal errors that block playback or payments | `logger.user.error` | Escalate with a clear message and include relevant IDs or URLs. |
| Metrics or noisy traces that only help during QA | `logger.dev.*` | Wrap expensive logging in conditional checks when possible. |
Avoid direct `console.*` calls—routing everything through the logger keeps the
production console clean and guarantees dev-mode gates behave consistently.
## Using the logger from inline scripts
The logger is exposed as a frozen global so inline scripts can share the same
API without additional bundling:
```js
const { dev, user } = window.bitvidLogger;
dev.log("rendering upload modal preview");
user.warn("Playback manifest missing expected resolution tag");
```
If you need to guard logic by environment, rely on the companion global set in
`js/config.js`:
```js
if (window.__BITVID_DEV_MODE__) {
window.bitvidLogger.dev.info("Hydrating admin widgets in dev mode");
}
```
The globals are populated as soon as `js/config.js` loads. Check for their
existence before using them in third-party contexts that may execute earlier.
## Adjusting the dev-mode flag for deployments
The source of truth lives in `config/instance-config.js`:
- Set `IS_DEV_MODE = true` while developing locally so the dev logger and other
diagnostics are available.
- Flip `IS_DEV_MODE = false` before shipping a production build. This change
propagates to `isDevMode` (the module export), `window.__BITVID_DEV_MODE__`,
and `logger.dev`, silencing development-only messages.
Always commit the flag change alongside deployment-ready configuration tweaks.
Forgetting to update the flag leaves verbose traces in user consoles and can
expose experimental behavior intended only for QA environments.
+174
View File
@@ -0,0 +1,174 @@
# Menu & Popover Integration
Menus, quick actions, and contextual overlays all flow through the shared popover
engine. This guide documents how to request the overlay root, create and manage
popovers, and apply the Tailwind overlay helpers that keep menus consistent with
the design system.
## Request the overlay root
Use `ensureOverlayRoot` before mounting any floating UI so the engine has a
stable portal target. The helper resolves the right `document` instance and
appends the root container when it is missing.
```js
import ensureOverlayRoot from "../js/ui/overlay/overlayRoot.js";
const doc = trigger?.ownerDocument || document;
const overlayRoot = ensureOverlayRoot(doc);
// overlayRoot is a `<div id="uiOverlay">` tagged with `data-overlay-root`.
// Popovers, sheets, and modals render inside this element.
```
`ensureOverlayRoot` is idempotent—call it during setup for each controller or
dev surface without worrying about duplicates.
## Create a popover
`createPopover(trigger, render, options)` wires focus management, aria
attributes, collision detection, and auto updates. The render callback receives
a portal `container` inside the overlay root. Return the panel element so the
engine toggles `data-popover-state` / `data-state` values while it opens and closes.
```js
import createPopover from "../js/ui/overlay/popoverEngine.js";
function attachMenu(trigger) {
const popover = createPopover(
trigger,
({ container, close }) => {
const panel = container.ownerDocument.createElement("div");
panel.className = "popover__panel menu space-y-1";
[
{ label: "Play", action: "play" },
{ label: "Save to playlist", action: "save" },
{ label: "Share", action: "share" },
].forEach(({ label, action }) => {
const item = container.ownerDocument.createElement("button");
item.type = "button";
item.className = "menu__item justify-between";
item.dataset.action = action;
item.textContent = label;
item.addEventListener("click", () => close({ restoreFocus: true }));
panel.appendChild(item);
});
container.append(panel);
return panel;
},
{
document: trigger?.ownerDocument,
placement: "bottom-end",
restoreFocusOnClose: true,
},
);
trigger.addEventListener("click", async (event) => {
event.preventDefault();
await popover.open();
});
}
```
Pass `document`, `placement`, and other floating-ui options through the third
argument. The helper exposes `open()`, `close()`, `toggle()`, `isOpen()`, and
`destroy()` for lifecycle control.
### Floating UI dependency
The shared popover engine standardizes on
[`@floating-ui/dom`](https://floating-ui.com/) for positioning. The module is
vendored to `vendor/floating-ui.dom.bundle.min.js` by
`npm run build:beacon`, which bundles dependencies and copies the upstream
license alongside `nostr-tools`. Run that build whenever you bump Floating UI so
the checked-in vendor artifact stays in sync. New overlays should lean on the
engine rather than re-implementing manual gap, flip, or viewport clamp math—the
middleware layer already applies the configured gap and viewport padding via
`offset()`, `flip()`, and `shift()` helpers.
## Tailwind overlay classes
Tailwind ships token-backed overlay helpers that keep surfaces aligned with the
palette:
- `popover__panel` / `popover-panel` — rounded overlay panel with opacity and
scale transitions. Add `menu` and `menu__item` classes for stacked button
menus.
- `overlay-scrim` and `overlay-panel` — lightweight surfaces for inline overlays
and frosted tooltips. Use the `data-strength` / `data-variant` modifiers to
switch tones.
- `ds-overlay-backdrop` — full-screen backdrop helper for async states.
- `bv-modal`, `bv-modal-backdrop`, and `bv-modal__panel` — modal scaffolding
used by upload/edit flows. Mobile breakpoints flatten the panel automatically.
Avoid ad-hoc opacity or blur utilities—let the semantic classes map to overlay
color tokens so theme swaps stay centralized in
`css/tailwind.source.css`.
## Video card actions
Video cards lean on `MoreMenuController` to attach a popover to the card's
"More" button. The controller registers the overlay root, builds the menu panel,
and opens the popover on demand.
```js
// js/ui/moreMenuController.js
const render = this.createPopoverRender(entry);
const popover = createPopover(trigger, render, {
document: documentRef,
placement: "bottom-end",
});
// Inside the render callback (createVideoMoreMenuPanel):
panel.className = "popover__panel menu space-y-1";
button.className = "menu__item";
```
Each menu button dispatches `video:context-action` events so cards, lists, and
player overlays can respond without duplicating menu markup.
## Modal actions
`VideoModal` follows the same pattern for its in-modal menu and zap actions. It
requests the overlay root from the modal's `ownerDocument`, builds a
`popover__panel`, and delegates action handling back to the controller.
```js
// js/ui/components/VideoModal.js
const render = ({ document: doc, close }) => {
const panel = this.buildModalMoreMenuPanel({ document: doc, close });
return panel;
};
const popover = createPopover(this.modalMoreBtn, render, {
document: documentRef,
placement: "bottom-end",
restoreFocusOnClose: true,
});
```
Modal action buttons reuse the same `menu__item` styling and call `close()` to
collapse the overlay before dispatching the selected action.
## Manual QA surface
A lightweight QA harness lives at `views/dev/popover-demo.html`. Launch the
static dev server from the repository root:
```bash
python -m http.server 8000
# or
npx serve
```
Then open `http://localhost:8000/views/dev/popover-demo.html` to interact with a
sample video card and modal wired to the popover engine. The page ensures the
overlay root exists, demonstrates Tailwind overlay helpers, and exercises both
card and modal action menus without relying on production data.
- ✅ **Anchored alignment check:** Open the video-card “More” menu and the
modal action menu. Each panel’s right edge should stay flush with the trigger
button. Playwright now verifies this via `tests/e2e/popover.spec.ts`, but QA
should still confirm the alignment while running the manual demo.
+44 -3
View File
@@ -1,7 +1,7 @@
# Bitvid Moderation Overview
# bitvid Moderation Overview
## What we’re building
Bitvid is follow-centric. Your Home feed comes from people you follow (F1). Discovery can optionally expand to friends-of-friends (F2) and ranked sources. Moderation is **client-side** and **user-controlled** with optional admin lists.
bitvid is follow-centric. Your Home feed comes from people you follow (F1). Discovery can optionally expand to friends-of-friends (F2) and ranked sources. Moderation is **client-side** and **user-controlled** with optional admin lists.
## Core principles
- **Freedom to choose**: User picks filters. Defaults are safe but reversible.
@@ -18,16 +18,57 @@ Bitvid is follow-centric. Your Home feed comes from people you follow (F1). Disc
- **Replies/threads**: NIP-10 (comments).
- **Counts**: NIP-45 (relay COUNT; optional fallbacks).
## Runtime flow (blur, hide, override)
- [`ModerationService`](../../js/services/moderationService.js) orchestrates ingest and scoring. Review the service for the trusted-report math and helper entry points.
- [`createModerationStage`](../../js/feedEngine/stages.js) wires the service into the feed engine where moderation summaries decorate timeline items.
- [`bitvidApp.decorateVideoModeration`](../../js/app.js) connects stage output to the UI layer alongside feature-flag plumbing.
- [`VideoCard.refreshModerationUi`](../../js/ui/components/VideoCard.js) applies badges, blur states, hide metadata (`data-moderation-hidden`), and the "show anyway" toggles.
## Where to extend
Thread new moderation behaviors through the same service → stage → app → UI flow above. Extending the existing layers keeps overrides, feature flags, and QA hooks consistent—avoid spinning up parallel moderation modules unless the architecture document explicitly calls for it.
## Defaults (policy)
- Blur video thumbnails if **≥ 3** F1 friends report `nudity`.
- Disable autoplay preview if **≥ 2** F1 friends report `nudity`.
- Hide videos when **≥ 1** trusted contact mutes the author (`TRUSTED_MUTE_HIDE_THRESHOLD`). Cards render with `data-moderation-hidden="true"`, the badge reads `Hidden · 1 trusted mute`, and a "Show anyway" button becomes available.
- Hide videos when **≥ 3** trusted contacts file spam reports (`TRUSTED_SPAM_HIDE_THRESHOLD`). The badge copy escalates to `Hidden · 3 trusted spam reports` and the card stays hidden until the viewer overrides it.
- Downrank author when any F1 has them in mute list (10000).
- Opt-in admin lists (30000 with `d=bitvid:admin:*`) can hard-hide content.
- Trust seeds from `DEFAULT_TRUST_SEED_NPUBS` count as baseline F1 contacts (toggle via `FEATURE_TRUST_SEEDS`).
> You can override all defaults in **Settings → Safety & Moderation**.
### Hide thresholds & feature flags
- `TRUSTED_MUTE_HIDE_THRESHOLD` and `TRUSTED_SPAM_HIDE_THRESHOLD` determine when moderation metadata marks an item as hidden. Both feed the `original.hide*` fields that `VideoCard` inspects before deciding whether to render the hidden summary container.
- `FEATURE_TRUSTED_HIDE_CONTROLS` shows the additional threshold inputs in the profile modal so operators can raise/lower the hide triggers client-side.
- When any hide threshold fires we persist the active counts (`hideCounts.trustedMuteCount`, `hideCounts.trustedReportCount`) so analytics/debugging tools can show why an item disappeared.
- Cards that arrive with `original.hidden === true` automatically expose the "Show anyway" chip so viewers can inspect hidden content. Activating the override clears blur/autoplay restrictions alongside the hidden state.
## Community blacklist federation
Operators can delegate hard-hide decisions to trusted curators without giving them full admin powers. The flow is:
1. The super admin maintains a `kind 30000` list with `d=${ADMIN_LIST_NAMESPACE}:${ADMIN_COMMUNITY_BLACKLIST_SOURCES}`. Each `a` tag references a community curator’s blacklist as `30000:<curator hex pubkey>:${ADMIN_LIST_NAMESPACE}:${ADMIN_COMMUNITY_BLACKLIST_PREFIX}:<slug>`.
2. Every curator publishes their own `kind 30000` list using the referenced `d` tag (`${ADMIN_LIST_NAMESPACE}:${ADMIN_COMMUNITY_BLACKLIST_PREFIX}:<slug>`) and fills it with `p` tags for accounts they want hidden.
3. bitvid automatically fetches each referenced list, merges the entries into the global blacklist, and removes any duplicates or guard-protected accounts (super admin, editors, or whitelist members).
To add a community list, append a new `a` tag to the super-admin source list and ask the curator to publish their companion `p` list. To remove a list, delete the `a` tag or ask the curator to clear their `p` entries; the client stops ingesting it on the next refresh.
New operators should also note that fresh viewer accounts automatically inherit the merged admin + community blacklist on their first login. The client seeds the user’s personal block list with that baseline once, so moderators can rely on a shared floor while still allowing operators to remove entries locally without having them reappear.
### Safety & Moderation controls
The profile modal now exposes blur, autoplay, and hide thresholds so operators can dial in stricter or more permissive behavior. Enter a non-negative whole number to override the default or leave the field blank to fall back to the baseline values above. Adjustments are stored locally, applied immediately to the active feed, and rehydrate on every load.
### Runtime flags
- `TRUSTED_MUTE_HIDE_THRESHOLD` — numeric default for the trusted mute hide control (default `1`).
- `TRUSTED_SPAM_HIDE_THRESHOLD` — numeric default for the trusted spam hide control (default `3`).
- `FEATURE_TRUSTED_HIDE_CONTROLS` — boolean toggle to hide/show the new trusted hide controls in the UI (default `true`).
## Files in this folder
- `web-of-trust.md` — how we compute trust signals and thresholds
- `nips.md` — exact NIPs and kinds Bitvid uses
- `nips.md` — exact NIPs and kinds bitvid uses
- `relays.md` — relay compatibility and COUNT fallbacks
- `testing.md` — QA checklist + test vectors
+624
View File
@@ -0,0 +1,624 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Moderation Fixtures • bitvid</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/css/tailwind.generated.css" />
<style>
body {
background-color: #0f172a;
color: #f8fafc;
font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
main {
max-width: 960px;
margin: 0 auto;
padding: 2rem 1.5rem 4rem;
}
h1 {
font-size: 2rem;
font-weight: 700;
margin-bottom: 1.5rem;
}
section {
margin-bottom: 2.5rem;
}
section h2 {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
section p {
color: #cbd5f5;
margin-bottom: 1rem;
}
</style>
</head>
<body data-ds="new">
<main id="moderationFixtureRoot">
<h1>Moderation Test Fixtures</h1>
<p>
These scenarios mirror the QA vectors in <code>docs/moderation/testing.md</code> so
automated checks can assert blur thresholds, autoplay blocking, and local
override persistence.
</p>
</main>
<script type="module">
import { VideoCard } from "/js/ui/components/VideoCard.js";
import moderationService from "/js/services/moderationService.js";
const STORAGE_KEY = "bitvid:moderation:fixture-overrides";
const fixtures = [
{
key: "blur-threshold",
heading: "Blur threshold (≥3 trusted nudity reports)",
description:
"Card should be blurred and autoplay disabled until the viewer overrides it.",
thresholds: { blur: 3, autoplay: 2 },
video: {
id: "1".repeat(64),
title: "Fixture: Blurred Preview",
pubkey: "a".repeat(64),
thumbnail: "/assets/jpg/video-thumbnail-fallback.jpg",
},
moderation: {
blockAutoplay: true,
blurThumbnail: true,
trustedCount: 3,
reportType: "nudity",
},
reporters: [
{ name: "Jess F1", pubkey: "b".repeat(64), latest: 1_700_000_010 },
{ name: "Mira F1", pubkey: "c".repeat(64), latest: 1_700_000_020 },
{ name: "Omar F1", pubkey: "d".repeat(64), latest: 1_700_000_030 },
],
blockToggle: {
label: "Simulate viewer blocking Mira F1",
pubkey: "c".repeat(64),
},
},
{
key: "autoplay-threshold",
heading: "Autoplay threshold (≥2 trusted nudity reports)",
description:
"Card should block autoplay but stay unblurred so users can inspect the thumbnail.",
video: {
id: "2".repeat(64),
title: "Fixture: Autoplay Blocked",
pubkey: "e".repeat(64),
thumbnail: "/assets/jpg/video-thumbnail-fallback.jpg",
},
moderation: {
blockAutoplay: true,
blurThumbnail: false,
trustedCount: 2,
reportType: "nudity",
},
reporters: [
{ name: "Jess F1", pubkey: "b".repeat(64), latest: 1_700_000_010 },
{ name: "Mira F1", pubkey: "c".repeat(64), latest: 1_700_000_020 },
],
},
{
key: "show-anyway",
heading: "\"Show anyway\" override (persists locally)",
description:
"Card starts blurred and blocked; activating \"Show anyway\" should persist across reloads.",
enableOverride: true,
video: {
id: "3".repeat(64),
title: "Fixture: Override Persistence",
pubkey: "f".repeat(64),
thumbnail: "/assets/jpg/video-thumbnail-fallback.jpg",
},
moderation: {
blockAutoplay: true,
blurThumbnail: true,
trustedCount: 3,
reportType: "nudity",
},
reporters: [
{ name: "Jess F1", pubkey: "b".repeat(64), latest: 1_700_000_010 },
{ name: "Mira F1", pubkey: "c".repeat(64), latest: 1_700_000_020 },
{ name: "Omar F1", pubkey: "d".repeat(64), latest: 1_700_000_030 },
],
},
{
key: "trusted-report-hide",
heading: "Trusted report hide (≥3 spam reports)",
description:
"Card should render hidden with spam copy until the viewer clicks \"Show anyway\".",
enableOverride: true,
thresholds: { reportHide: 3 },
video: {
id: "4".repeat(64),
title: "Fixture: Trusted Report Hide",
pubkey: "aa".repeat(32),
thumbnail: "/assets/jpg/video-thumbnail-fallback.jpg",
},
moderation: {
blockAutoplay: true,
blurThumbnail: true,
trustedCount: 3,
reportType: "spam",
},
reporters: [
{ name: "Jess F1", pubkey: "b".repeat(64), latest: 1_700_000_010 },
{ name: "Mira F1", pubkey: "c".repeat(64), latest: 1_700_000_020 },
{ name: "Omar F1", pubkey: "d".repeat(64), latest: 1_700_000_030 },
],
},
{
key: "trusted-mute-hide",
heading: "Trusted mute hide (F1 mute list threshold)",
description:
"Card should default to hidden when a trusted contact mutes the author and allow an override.",
enableOverride: true,
thresholds: { muteHide: 1 },
video: {
id: "5".repeat(64),
title: "Fixture: Trusted Mute Hide",
pubkey: "bb".repeat(32),
thumbnail: "/assets/jpg/video-thumbnail-fallback.jpg",
},
moderation: {
blockAutoplay: false,
blurThumbnail: false,
trustedMuted: true,
trustedMuteCount: 1,
},
muters: [{ name: "Jess F1", pubkey: "b".repeat(64) }],
},
];
const blockedReportersByVideo = new Map();
function getBlockedSet(videoId) {
if (typeof videoId !== "string" || !videoId) {
return null;
}
return blockedReportersByVideo.get(videoId) || null;
}
function setReporterBlocked(videoId, pubkey, blocked) {
if (typeof videoId !== "string" || !videoId || typeof pubkey !== "string" || !pubkey) {
return;
}
let entry = blockedReportersByVideo.get(videoId);
if (blocked) {
if (!entry) {
entry = new Set();
blockedReportersByVideo.set(videoId, entry);
}
entry.add(pubkey);
return;
}
if (!entry) {
return;
}
entry.delete(pubkey);
if (!entry.size) {
blockedReportersByVideo.delete(videoId);
}
}
function isReporterBlocked(videoId, pubkey) {
const entry = getBlockedSet(videoId);
if (!entry) {
return false;
}
return entry.has(pubkey);
}
function getActiveReporters(fixture, videoId) {
const reporters = Array.isArray(fixture?.reporters) ? fixture.reporters : [];
if (!reporters.length) {
return [];
}
const entry = getBlockedSet(videoId);
if (!entry || !entry.size) {
return reporters.slice();
}
return reporters.filter((reporter) => {
const key = typeof reporter?.pubkey === "string" ? reporter.pubkey : "";
if (!key) {
return false;
}
return !entry.has(key);
});
}
function normalizeFixtureReporters(reporters) {
if (!Array.isArray(reporters)) {
return [];
}
return reporters
.map((reporter) => {
if (!reporter || typeof reporter !== "object") {
return null;
}
const pubkey = typeof reporter.pubkey === "string" ? reporter.pubkey : "";
if (!pubkey) {
return null;
}
const latest = Number.isFinite(reporter.latest) ? Math.floor(reporter.latest) : 0;
const name = typeof reporter.name === "string" ? reporter.name : "";
return { name, pubkey, latest };
})
.filter(Boolean);
}
window.__MODERATION_FIXTURE_STORAGE_KEY__ = STORAGE_KEY;
function loadOverrides() {
try {
const raw = window.localStorage?.getItem(STORAGE_KEY);
if (!raw) {
return {};
}
const parsed = JSON.parse(raw);
return parsed && typeof parsed === "object" ? parsed : {};
} catch {
return {};
}
}
function persistOverrides(next) {
try {
window.localStorage?.setItem(STORAGE_KEY, JSON.stringify(next));
} catch {
/* ignore persistence issues in test fixtures */
}
}
function buildSummary(fixture, videoId, sanitizedReporters = null) {
if (!fixture?.moderation?.reportType) {
return null;
}
const reporters =
Array.isArray(sanitizedReporters) && sanitizedReporters
? sanitizedReporters
: normalizeFixtureReporters(getActiveReporters(fixture, videoId));
const latest = reporters.reduce((max, reporter) => Math.max(max, reporter.latest), 0);
return {
eventId: fixture.video.id,
totalTrusted: reporters.length,
types: {
[fixture.moderation.reportType]: {
trusted: reporters.length,
total: reporters.length,
latest,
},
},
updatedAt: Date.now(),
};
}
function buildModerationState(fixture, overrides, videoId) {
const overrideEntry = overrides?.[videoId];
const activeReporters = getActiveReporters(fixture, videoId);
const sanitizedReporters = normalizeFixtureReporters(activeReporters);
const thresholds =
fixture.thresholds && typeof fixture.thresholds === "object"
? { blur: 3, autoplay: 2, reportHide: 3, muteHide: 1, ...fixture.thresholds }
: { blur: 3, autoplay: 2, reportHide: 3, muteHide: 1 };
const autoplayThreshold = Number.isFinite(thresholds.autoplay)
? Math.max(0, Math.floor(thresholds.autoplay))
: 2;
const blurThreshold = Number.isFinite(thresholds.blur)
? Math.max(0, Math.floor(thresholds.blur))
: 3;
const reportHideThreshold = Number.isFinite(thresholds.reportHide)
? Math.max(0, Math.floor(thresholds.reportHide))
: 3;
const muteHideThreshold = Number.isFinite(thresholds.muteHide)
? Math.max(0, Math.floor(thresholds.muteHide))
: 1;
const initialBlock = fixture.moderation.blockAutoplay === true;
const initialBlur = fixture.moderation.blurThumbnail === true;
const trustedCount = sanitizedReporters.length;
const reporterDisplayNames = sanitizedReporters
.map((reporter) => reporter.name)
.filter((value) => typeof value === "string" && value);
const reporterPubkeys = sanitizedReporters.map((reporter) => reporter.pubkey);
const trustedReporterEntries = sanitizedReporters.map((reporter) => ({
pubkey: reporter.pubkey,
latest: reporter.latest,
}));
const muterEntries = Array.isArray(fixture.muters) ? fixture.muters : [];
const muterPubkeys = muterEntries
.map((muter) => (typeof muter?.pubkey === "string" ? muter.pubkey : ""))
.filter(Boolean);
const muterDisplayNames = muterEntries
.map((muter) => (typeof muter?.name === "string" ? muter.name : ""))
.filter(Boolean);
const trustedMuted = fixture.moderation.trustedMuted === true;
const muteCount = Number.isFinite(fixture.moderation.trustedMuteCount)
? Math.max(0, Math.floor(fixture.moderation.trustedMuteCount))
: muterPubkeys.length;
const hideFromMute = trustedMuted && muteCount >= muteHideThreshold;
const hideFromReports = trustedCount >= reportHideThreshold;
let hidden = fixture.moderation.hidden === true;
let hideReason =
typeof fixture.moderation.hideReason === "string"
? fixture.moderation.hideReason
: "";
const hideBypass =
typeof fixture.moderation.hideBypass === "string"
? fixture.moderation.hideBypass
: "";
if (hideFromMute) {
hidden = true;
hideReason = "trusted-mute-hide";
} else if (hideFromReports) {
hidden = true;
hideReason = "trusted-report-hide";
} else if (!hidden) {
hideReason = "";
}
const hideCounts = hidden
? {
trustedMuteCount: muteCount,
trustedReportCount: trustedCount,
}
: null;
const base = {
reportType: fixture.moderation.reportType,
trustedCount,
reporterDisplayNames,
reporterPubkeys,
trustedReporters: trustedReporterEntries,
trustedMuted,
trustedMuters: muterPubkeys,
trustedMuteCount: muteCount,
trustedMuterDisplayNames: muterDisplayNames,
};
const summary = buildSummary(fixture, videoId, sanitizedReporters);
if (summary) {
base.summary = summary;
}
const computedBlock = initialBlock && trustedCount >= autoplayThreshold;
const computedBlur = initialBlur && trustedCount >= blurThreshold;
const originalState = {
blockAutoplay: computedBlock,
blurThumbnail: computedBlur,
hidden,
hideReason: hidden ? hideReason : "",
hideCounts: hideCounts
? {
trustedMuteCount: Math.max(0, Number(hideCounts.trustedMuteCount) || 0),
trustedReportCount: Math.max(0, Number(hideCounts.trustedReportCount) || 0),
}
: null,
hideBypass,
hideTriggered: hidden,
};
base.original = {
blockAutoplay: originalState.blockAutoplay,
blurThumbnail: originalState.blurThumbnail,
hidden: originalState.hidden,
hideReason: originalState.hideReason,
hideCounts: originalState.hideCounts,
hideBypass: originalState.hideBypass,
hideTriggered: originalState.hideTriggered,
};
if (overrideEntry?.showAnyway) {
base.viewerOverride = {
showAnyway: true,
updatedAt: Number.isFinite(overrideEntry.updatedAt)
? Math.floor(overrideEntry.updatedAt)
: Date.now(),
};
base.blockAutoplay = false;
base.blurThumbnail = false;
base.hidden = false;
if (base.hideReason) {
delete base.hideReason;
}
if (base.hideCounts) {
delete base.hideCounts;
}
if (base.hideBypass) {
delete base.hideBypass;
}
} else {
base.blockAutoplay = originalState.blockAutoplay;
base.blurThumbnail = originalState.blurThumbnail;
if (originalState.hidden) {
base.hidden = true;
if (originalState.hideReason) {
base.hideReason = originalState.hideReason;
}
if (originalState.hideCounts) {
base.hideCounts = { ...originalState.hideCounts };
}
if (originalState.hideBypass) {
base.hideBypass = originalState.hideBypass;
}
} else {
if (base.hidden) {
delete base.hidden;
}
if (base.hideReason) {
delete base.hideReason;
}
if (base.hideCounts) {
delete base.hideCounts;
}
if (base.hideBypass) {
delete base.hideBypass;
}
}
}
return base;
}
function setOverride(overrides, videoId, entry) {
const next = { ...overrides };
next[videoId] = {
...entry,
updatedAt: Number.isFinite(entry?.updatedAt)
? Math.floor(entry.updatedAt)
: Date.now(),
};
persistOverrides(next);
return next;
}
function createCard(fixture, overrides) {
const video = {
...fixture.video,
};
video.moderation = buildModerationState(fixture, overrides, video.id);
const card = new VideoCard({
document,
video,
shareUrl: "#",
formatters: {
formatTimeAgo: () => "just now",
formatNumber: (value) => {
if (typeof value === "number") {
return value.toLocaleString("en-US");
}
return String(value ?? "");
},
},
helpers: {
isMagnetSupported: () => true,
escapeHtml: (value) => String(value ?? ""),
toLocaleString: (value, locales, options) => {
if (value && typeof value.toLocaleString === "function") {
return value.toLocaleString(locales || "en-US", options);
}
return String(value ?? "");
},
},
assets: {
fallbackThumbnailSrc: "/assets/jpg/video-thumbnail-fallback.jpg",
},
designSystem: {
getMode: () => "new",
isNew: () => true,
},
});
const refreshModeration = () => {
card.video.moderation = buildModerationState(fixture, overrides, video.id);
if (typeof card.refreshModerationUi === "function") {
card.refreshModerationUi();
}
};
if (fixture.enableOverride) {
card.onModerationOverride = () => {
overrides = setOverride(overrides, video.id, { showAnyway: true });
refreshModeration();
return true;
};
}
const root = card.getRoot();
root.dataset.testId = fixture.key;
return { card, root, refreshModeration };
}
function renderFixtures() {
const root = document.getElementById("moderationFixtureRoot");
if (!root) {
return;
}
let overrides = loadOverrides();
fixtures.forEach((fixture) => {
const section = document.createElement("section");
const heading = document.createElement("h2");
heading.textContent = fixture.heading;
section.appendChild(heading);
if (fixture.description) {
const description = document.createElement("p");
description.textContent = fixture.description;
section.appendChild(description);
}
const { root: cardRoot, refreshModeration } = createCard(fixture, overrides);
section.appendChild(cardRoot);
if (fixture.blockToggle && fixture.blockToggle.pubkey) {
const toggleWrapper = document.createElement("div");
toggleWrapper.style.marginTop = "0.75rem";
toggleWrapper.style.fontSize = "0.875rem";
toggleWrapper.style.color = "#cbd5f5";
const label = document.createElement("label");
label.style.display = "inline-flex";
label.style.alignItems = "center";
label.style.gap = "0.5rem";
const input = document.createElement("input");
input.type = "checkbox";
input.checked = isReporterBlocked(fixture.video.id, fixture.blockToggle.pubkey);
input.addEventListener("change", () => {
const checked = input.checked;
setReporterBlocked(fixture.video.id, fixture.blockToggle.pubkey, checked);
const action = checked ? "block" : "unblock";
try {
const detail = { action, targetPubkey: fixture.blockToggle.pubkey };
const maybePromise = moderationService?.queueUserBlockRefresh?.(detail);
if (maybePromise && typeof maybePromise.then === "function") {
maybePromise.catch(() => {});
}
} catch {
/* ignore fixture errors */
}
refreshModeration();
});
const text = document.createElement("span");
text.textContent =
typeof fixture.blockToggle.label === "string"
? fixture.blockToggle.label
: "Viewer blocks reporter";
label.appendChild(input);
label.appendChild(text);
toggleWrapper.appendChild(label);
section.appendChild(toggleWrapper);
}
root.appendChild(section);
});
}
renderFixtures();
</script>
</body>
</html>
+1 -1
View File
@@ -1,4 +1,4 @@
# NIPs Bitvid uses (Moderation-Relevant)
# NIPs bitvid uses (Moderation-Relevant)
## Required
- **NIP-56 — Reporting**
+25 -5
View File
@@ -2,15 +2,26 @@
## Quick checklist
- [ ] F1 set cached and used for “trusted reports”.
- Automated coverage: `tests/moderation/trusted-report-count.test.mjs` (run via `npm run test:unit`).
- [ ] Blur at ≥3 F1 reports of `nudity`; autoplay blocked at ≥2.
- [ ] “Show anyway” works and records only local pref.
- Automated coverage: `tests/visual/moderation.spec.ts` blur and autoplay scenarios (run via `npm run test:visual`).
- [ ] Hide videos once trusted mute/spam thresholds are met.
- Automated coverage: `tests/visual/moderation.spec.ts` trusted hide scenarios (run via `npm run test:visual`).
- Manual: adjust thresholds in the profile modal, simulate trusted mutes/reports, and verify cards disappear until "show anyway" is used.
- [ ] “Show anyway” works for blurred and hidden cards and records only local pref.
- Automated coverage: `tests/visual/moderation.spec.ts` show-anyway persistence and trusted hide scenarios (run via `npm run test:visual`).
- [ ] Mute list (10000) downranks/hides consistently.
- Manual: subscribe to fixture B and verify muted author disappears from feeds after refresh.
- [ ] Admin lists (30000) take effect only when subscribed.
- Manual: load fixture C, toggle subscription, and confirm the hard-hide only applies while subscribed.
- [ ] COUNT fallback shows placeholders and doesn’t crash UI.
- Manual: point the client at a relay without COUNT support and confirm placeholders render instead of errors.
- [ ] Comment threads render with NIP-10; moderation badges show in context.
- Manual: open a reported thread and confirm badges + reply ordering remain intact.
- [ ] Reports from muted reporters are ignored.
- Automated coverage: `tests/moderation/trusted-report-count.test.mjs` (muted reporter case).
## Fixtures (create on test relays)
## Fixtures (create on test relays / served in `/docs/moderation/fixtures/`)
### A) NIP-56 report events
Create three distinct F1 reporter keys; each sends:
@@ -30,7 +41,9 @@ Expected:
* After 1 report → no blur; autoplay allowed.
* After 2 reports → autoplay blocked.
* After 3 reports → thumbnail blurred; reason chip shown.
* After 3 reports → thumbnail blurred; reason chip shown. At the spam threshold (`TRUSTED_SPAM_HIDE_THRESHOLD`) the card hides entirely and the badge copy switches to "Hidden · 3 trusted spam reports" until the viewer overrides it.
**Exercises:** [`ModerationService.ingestReportEvent()`](../../js/services/moderationService.js) and [`ModerationService.getTrustedReportSummary()`](../../js/services/moderationService.js) wire through [`createModerationStage()`](../../js/feedEngine/stages.js) so [`bitvidApp.decorateVideoModeration()`](../../js/app.js) can hand the summary to [`VideoCard.refreshModerationUi()`](../../js/ui/components/VideoCard.js).
### B) Mute list (10000)
@@ -43,7 +56,9 @@ Expected:
}
```
Expected: author’s items are downranked/hidden for this viewer.
Expected: author’s items are downranked/hidden for this viewer. When the mute threshold (`TRUSTED_MUTE_HIDE_THRESHOLD`) is met, cards render with "Hidden · 1 trusted mute" and require a "Show anyway" override to display content.
**Exercises:** [`ModerationService.ingestTrustedMuteEvent()`](../../js/services/moderationService.js) updates [`ModerationService.isAuthorMutedByTrusted()`](../../js/services/moderationService.js) / [`getTrustedMutersForAuthor()`](../../js/services/moderationService.js), which [`createModerationStage()`](../../js/feedEngine/stages.js) propagates to the video item before [`VideoCard.refreshModerationUi()`](../../js/ui/components/VideoCard.js) renders the mute state.
### C) Admin blacklist (30000)
@@ -59,13 +74,18 @@ Expected: author’s items are downranked/hidden for this viewer.
Expected: when viewer subscribes to this list, author is hard-hidden.
**Exercises:** [`accessControl`](../../js/accessControl.js) hydration feeds [`ModerationService.getAdminListSnapshot()`](../../js/services/moderationService.js) / [`getAccessControlStatus()`](../../js/services/moderationService.js), giving [`createModerationStage()`](../../js/feedEngine/stages.js) the whitelist/blacklist flags that [`bitvidApp.decorateVideoModeration()`](../../js/app.js) and [`VideoCard.refreshModerationUi()`](../../js/ui/components/VideoCard.js) respect.
### D) COUNT fallback
* Use a relay that doesn’t support COUNT.
* Verify “—” placeholders; no spinner loops; no crashes.
**Exercises:** Missing COUNT data flows through [`ModerationService.setActiveEventIds()`](../../js/services/moderationService.js) → [`ModerationService.getTrustedReportSummary()`](../../js/services/moderationService.js) defaults, letting [`createModerationStage()`](../../js/feedEngine/stages.js) emit neutral moderation metadata that [`VideoCard.refreshModerationUi()`](../../js/ui/components/VideoCard.js) renders as placeholders.
## Automated checks (suggested)
* Unit test `trustedReportCount`.
* `npm run test:unit` — covers `tests/moderation/trusted-report-count.test.mjs` for deduping and mute handling.
* `npm run test:visual` — runs Playwright fixtures in `tests/visual/moderation.spec.ts` against `/docs/moderation/fixtures/index.html`, covering blur thresholds, autoplay blocks, trusted hide badges, and the override UX.
* Integration test: emit events over a local relay; assert UI state.
* Regression test: ensure Home feed ignores reputation gating.
+17 -1
View File
@@ -1,4 +1,4 @@
# Web-of-Trust Policy (Bitvid)
# Web-of-Trust Policy (bitvid)
## Graph terms
- **F1 (friends)**: pubkeys you follow.
@@ -16,6 +16,8 @@
- `blurThumbnail = trustedReportCount(event,'nudity') >= 3`
- `hideAutoplay = trustedReportCount(event,'nudity') >= 2`
- `downrankIfMutedByF1 = true`
- `hideIfTrustedMuteCount(author) >= 1`
- `hideIfTrustedSpamReports(event) >= 3`
### Why these numbers?
- F1-only reports resist Sybil attacks.
@@ -28,6 +30,12 @@
- `['d','bitvid:admin:editors']` → trusted channel editors.
- Users can subscribe/unsubscribe any time.
### Decision precedence
1. **Personal blocks win first.** If a viewer blocks an author or reporter, we ignore their content and reports regardless of admin lists.
2. **Admin blacklist applies next.** Entries on `bitvid:admin:blacklist` are hard-hidden and their reports suppressed before looking at thresholds.
3. **F1 thresholds run last.** Blur/autoplay gating only evaluates trusted-report counts after personal blocks and admin blacklists. Admin whitelists may bypass Discovery gating, but never override a viewer block.
## Pseudocode
```ts
@@ -50,6 +58,14 @@ function shouldBlurThumb(eventId: string, ctx: Ctx): boolean {
function shouldHideAutoplay(eventId: string, ctx: Ctx): boolean {
return trustedReportCount(eventId, 'nudity', ctx.viewerFollows, ctx.reports) >= 2;
}
function shouldHideForTrustedMute(authorHex: string, ctx: Ctx): boolean {
return ctx.trustedMuteCount(authorHex) >= 1;
}
function shouldHideForTrustedSpam(eventId: string, ctx: Ctx): boolean {
return trustedReportCount(eventId, 'spam', ctx.viewerFollows, ctx.reports) >= 3;
}
```
## UI rules
+2 -2
View File
@@ -1,6 +1,6 @@
# Nostr Analytics Knobs
BitVid's view counter emits Nostr events so operators can track engagement without duplicating storage. Tune the following exports in `config/instance-config.js` to match your retention and performance goals:
bitvid's view counter emits Nostr events so operators can track engagement without duplicating storage. Tune the following exports in `config/instance-config.js` to match your retention and performance goals:
- `VIEW_COUNT_DEDUPE_WINDOW_SECONDS` (default: `86_400`): repeat plays from the same viewer inside this window are treated as duplicates so stalled reloads do not inflate totals. Shorten the window to count more aggressive replays, or extend it if you want conservative numbers.
- `VIEW_COUNT_BACKFILL_MAX_DAYS` (default: `90`): controls how far back hydrators should walk history when a new analytics worker boots. Longer windows deliver deeper trend lines at the cost of heavier relay scans.
@@ -16,7 +16,7 @@ Playback telemetry runs even when a viewer has not connected a Nostr account. Th
### Publish latency expectations
When a player crosses the 12-second watch threshold, BitVid invokes [`publishViewEvent`](../js/nostr.js#L2159-L2323) through the view-only [`recordVideoView`](../js/nostr.js#L4516-L4555) contract. The helper considers the publish successful as soon as any configured relay acknowledges the event, which in practice keeps latency under a second on public relays. The UI optimistically increments totals by feeding the signed event into [`ingestLocalViewEvent`](../js/viewCounter.js#L608-L640), so cards and modals reflect the new view immediately even while other relays finish syncing.
When a player crosses the 12-second watch threshold, bitvid invokes [`publishViewEvent`](../js/nostr.js#L2159-L2323) through the view-only [`recordVideoView`](../js/nostr.js#L4516-L4555) contract. The helper considers the publish successful as soon as any configured relay acknowledges the event, which in practice keeps latency under a second on public relays. The UI optimistically increments totals by feeding the signed event into [`ingestLocalViewEvent`](../js/viewCounter.js#L608-L640), so cards and modals reflect the new view immediately even while other relays finish syncing.
The watch history pipeline has been retired; `recordVideoView` now focuses solely on analytics publishing.
Loaded 100 of 277 files, more files were not shown because too many files have changed in this diff. Show more