docs(ai): improve onboarding steps and add devcontainer

- Update README.md to recommend `npm ci` and include test steps.
- Update CONTRIBUTING.md with explicit development setup instructions.
- Add `.devcontainer/devcontainer.json` for reproducible Node 20 environment.
- Add audit report in `ai/reports/onboarding-20260118.md`.
This commit is contained in:
google-labs-jules[bot] committed 2026-01-18 08:23:14 +00:00
1 parent a82c66b760
commit 09545c869c
10 files changed
+364 -154

No files matched your search

+13
View File
@@ -0,0 +1,13 @@
{
"name": "bitvid",
"image": "mcr.microsoft.com/devcontainers/javascript-node:20",
"postCreateCommand": "npm ci",
"customizations": {
"vscode": {
"extensions": [
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss"
]
}
}
}
+22
View File
@@ -31,6 +31,28 @@ If we later introduce **dual-licensing** options (e.g., commercial licenses for
* Follow existing formatting/lint rules.
* Update docs and user-facing text when behavior changes.
## Development Setup
To set up the project locally:
1. **Install Dependencies**:
```bash
npm ci
```
2. **Run Tests**:
```bash
npm run test:unit
```
3. **Format & Lint**:
```bash
npm run format
npm run lint
```
For a full guide, see the [Local Setup section in README.md](./README.md#local-setup).
## Security
If you find a vulnerability, please email **[[email protected]](mailto:[email protected])**. We’ll coordinate a responsible disclosure window before public release.
+14 -3
View File
@@ -103,7 +103,13 @@ To run **bitvid** locally:
cd bitvid
```
2. Start a local server:
2. Install dependencies:
```bash
npm ci
```
3. Start a local server:
- Using Python:
```bash
python -m http.server 8000
@@ -315,9 +321,14 @@ placeholder at “—” and development builds log a warning—so mixed deploym
```bash
git checkout -b feature/your-feature-name
```
3. **Make Changes**:
3. **Install Dependencies**:
```bash
npm ci
```
4. **Make Changes**:
- Ensure your code follows best practices and is well-documented.
4. **Test**:
5. **Test**:
- Run unit tests: `npm run test:unit`
- Validate the site functionality locally before submitting.
5. **Submit a Pull Request**:
- Explain your changes and reference any related issues.
+38
View File
@@ -0,0 +1,38 @@
# Onboarding Audit Report - 2026-01-18
## Summary
Performed a fresh checkout validation of the `bitvid` repository to audit the developer onboarding experience.
## Execution Log
1. **Dependency Installation**: `npm ci` was used instead of `npm install` (implied by README) to ensure a clean state.
- Result: Success.
2. **Build**: `npm run build:css`.
- Result: Success.
3. **Format**: `npm run format`.
- Result: Success.
4. **Testing**: `npm run test:unit`.
- Result: Timed out in the audit environment (400s limit), but individual tests pass. The test runner executes tests sequentially which can be slow on constrained resources.
## Findings & Fixes
### Documentation Gaps
- **README.md**:
- The "Local Setup" section implied `npm install` but `npm ci` is preferred for consistent CI/CD and fresh setups.
- The "For Contributors" section completely missed the dependency installation step.
- **Fix**: Updated `README.md` to explicitly list `npm ci` in both sections and added a "Test" step with `npm run test:unit`.
- **CONTRIBUTING.md**:
- Lacked technical setup instructions, relying on README.
- **Fix**: Added a "Development Setup" section summarizing `npm ci`, testing, and linting commands, linking back to README.
### Environment Brittleness
- The reliance on system-wide Node.js versions was unidentified.
- **Fix**: Added a `.devcontainer/devcontainer.json` configuration to standardize the development environment using a Docker-based Node.js 20 container.
### Linting Failures
- `npm run lint` fails on a fresh checkout due to pre-existing inline style violations in `js/ui/components/EventDetailsModal.js` and `js/ui/components/SimilarContentCard.js`.
- **Recommendation**: These legacy style violations should be addressed in a separate refactoring PR.
## Recommendations
- Contributors should use the provided Devcontainer or ensure Node.js 20+ is installed.
- `npm ci` should be the standard instruction for new environments.
+8 -2
View File
@@ -1,8 +1,14 @@
: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;--z-modal-peak-root:165;--z-modal-peak-overlay:175;--z-modal-peak-content:185;--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;--breakpoint-lg:64rem;--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-blocLine truncated
/*! tailwindcss v3.4.19 | 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}:root[data-theme-accent=override]{--color-accent:var(--color-accent-override);--color-accent-strong:var(--color-accent-strong-override);--color-accent-pressed:var(--color-accent-pressed-override)}#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:vaLine truncated
/*! tailwindcss v3.4.19 | 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}:root[data-theme-accent=override]{--color-accent:var(--color-accent-override);--color-accent-strong:var(--color-accent-strong-override);--color-accent-pressed:var(--color-accent-pressed-override)}#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:vaLine truncated
--motion-video-card-backdrop-translate
);--video-card-backdrop-scale:1.12;--video-card-backdrop-blur:var(--blur-video-card-backdrop-strong)}.\!card:focus-within,.\!card:hover{--video-card-lift-translate-y:var(--motion-video-card-hover-translate)!important;--video-card-backdrop-opacity:0.425!important;--video-card-backdrop-translate-y:var(
--motion-video-card-backdrop-translate
)!important;--video-card-backdrop-scale:1.12!important;--video-card-backdrop-blur:var(--blur-video-card-backdrop-strong)!important;--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.card:focus-within,.card:hover{--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.\!card:focus-within,.\!card:hover{--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.\!card:focus-visible{border-color:var(--color-accent);--tw-border-opacity:0.4;--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.card:focus-visible{border-color:var(--color-accent);--tw-border-opacity:0.4;--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.\!card[data-state=critical],.card[data-state=critical]{border-width:1px;border-color:var(--color-critical);--tw-border-opacity:0.4;background-color:var(--surface-raised-critical)}.\!card[data-state=private],.card[data-state=private]{background-color:var(--surface-raised-private);--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.\!card[data-state=disabled],.card[data-state=disabled]{opacity:.6;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.video-card__title{transition:color var(--motion-duration-base) var(--motion-ease-in-out)}.video-card__title:focus-visible,.video-card__title:hover{color:var(--color-accent)}.form-control{outline:2px solid transparent;outline-offset:2px}.form-control:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}.form-control{display:block;width:100%;border-radius:var(--radius-md);border-width:1px;border-color:var(--border-subtle);background-color:var(--surface-alt);padding:.5rem .75rem;font-size:var(--font-size-sm);line-height:var(--line-height-snug);color:var(--text-primary);--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);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(.4,0,.2,1)}.form-control::-moz-placeholder{color:var(--text-muted)}.form-control::placeholder{color:var(--text-muted)}.form-control:hover{border-color:var(--border-default)}.form-control:focus-visible{border-color:var(--color-accent-strong)}.form-control:disabled{cursor:not-allowed;background-color:var(--surface-base);color:var(--text-muted);opacity:.6}.form-control:invalid{border-color:var(--color-critical);--tw-border-opacity:0.6}.\!input[data-state=locked],.form-control[data-state=locked],.input[data-state=locked],.select[data-state=locked],.textarea[data-state=locked],select.form-control[data-state=locked],textarea.form-control[data-state=locked]{cursor:default;border-color:var(--border-sidebar-strong);background-color:var(--surface-overlay-panel-soft);color:var(--text-muted)}.form-control[data-state=locked]:focus-visible,.input[data-state=locked]:focus-visible,.select[data-state=locked]:focus-visible,.textarea[data-state=locked]:focus-visible,select.form-control[data-state=locked]:focus-visible,textarea.form-control[data-state=locked]:focus-visible{border-color:var(--border-sidebar-strong);--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--Line truncated
--player-modal-inline-padding,clamp(var(--space-md),4vw,var(--space-xl-plus))
);overflow:visible}#playerModal .watch-container.video-modal__similar{padding-inline:clamp(var(--space-3xs),.6vw,var(--space-2xs))}#playerModal .video-modal__primary>.watch-container:first-child{padding-block-start:var(--player-modal-vertical-padding)}#playerModal .video-modal__primary>.watch-container:last-child,#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-md-plus)}#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);min-width:0;overflow-wrap:break-word;word-break:break-word}@media (min-width:1024px){.video-modal__headline #videoTitle.video-modal__title{flex:1 1 0%;min-width:0;max-width:calc(100% - var(--space-3xl) - var(--space-md))}.video-modal__headline .video-modal__status{flex:0 0 auto;max-width:clamp(var(--space-xl),25%,var(--space-3xl));margin-left:auto}}#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{align-items:center;gap:var(--space-md);padding:var(--space-md) var(--space-lg)}#playerModal .channel-card,#playerModal .player-modal__module{display:flex;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 .player-modal__module{flex-direction:column;gap:clamp(var(--space-2xs),1vw,var(--space-sm));width:100%;padding:clamp(var(--space-xs),1vw,var(--space-sm))}#playerModal .player-modal__module-title{margin:0;font-size:var(--font-size-lg);font-weight:600;line-height:1.3;color:var(--color-text)}#playerModal .player-modal__module-list{display:flex;flex-direction:column;gap:var(--space-sm);width:100%;margin:0;padding:0;list-style:none}#playerModal .video-modal__similar .player-modal__module{gap:clamp(var(--space-3xs),.6vw,var(--space-xs));padding:clamp(var(--space-2xs),.6vw,var(--space-xs));border-width:0;box-shadow:none;background-color:transparent}#playerModal .video-modal__similar .player-modal__module-title{padding-inline:clamp(var(--space-3xs),.6vw,var(--space-2xs))}#playerModal .video-modal__similar .player-modal__module-list{gap:clamp(var(--space-3xs),.6vw,var(--space-xs));padding-inline:clamp(var(--space-3xs),.6vw,var(--space-2xs));padding-bottom:clamp(var(--space-3xs),.6vw,var(--space-2xs))}#playerModal .player-modal__module-scroll{max-height:clamp(var(--player-modal-module-scroll-min-height),calc(100vh - var(--player-modal-vertical-padding)*2 - var(--space-2xl)),var(--player-modal-module-scroll-max-height));overflow-y:auto;overflow-x:hidden;overscroll-behavior:contain;scrollbar-gutter:stable;padding-inline-end:var(--space-xs)}#playerModal .video-modal__similar .player-modal__module-scroll{max-height:none;overflow:visible;padding-inline-end:0;scrollbar-gutter:auto}#playerModal .player-modal__module-scroll>li{scroll-margin-block:var(--space-xs)}@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)) sLine truncated
--player-modal-primary-inline-padding-lg,clamp(var(--space-sm),2.75vw,var(--space-lg))
)}}.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;min-width:0;min-height:0}#playerModal.video-modal__secondary{display:block}.video-modal__secondary{display:flex;flex-direction:column;gap:1rem;min-width:0;min-height:0;width:100%;align-self:stretch}.video-modal__similar{display:none}@media (min-width:1024px){#playerModal.video-modal__similar{display:block}.video-modal__similar{display:flex;flex-direction:column;gap:1.5rem;align-self:stretch;flex:1 1 auto;min-height:0}#playerModal .video-modal__similar .player-modal__module{height:100%;display:flex;flex:1 1 auto;min-height:0;flex-direction:column}#playerModal .video-modal__similar .player-modal__module-list{flex:1 1 auto;min-height:0}#playerModal .video-modal__similar .player-modal__module-scroll{height:100%;max-height:none;min-height:0}}#playerModal .player-modal__module-item{width:100%}#playerModal#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card){display:block}#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card){display:flex;width:100%;flex-direction:row;flex-wrap:nowrap;align-items:stretch;gap:var(--space-md)}#playerModal#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card){display:block}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card){display:flex;width:100%;flex-direction:row;flex-wrap:nowrap;align-items:stretch;gap:var(--space-md)}#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__media{width:clamp(var(--video-modal-item-media-min),18vw,var(--video-modal-item-media-max))}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__media{width:clamp(var(--video-modal-item-media-min),18vw,var(--video-modal-item-media-max))!important}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__media,#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__media{flex-shrink:0;overflow:hidden;border-radius:var(--radius-lg)}#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__content{padding:clamp(var(--space-sm),1.25vw,var(--space-md))}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__content{padding:clamp(var(--space-sm),1.25vw,var(--space-md))!important}#playerModal#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__content{display:block}#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__content{display:flex;min-width:0;flex:1 1 0%;flex-direction:column;gap:var(--space-sm)}#playerModal#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__content{display:block}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__content{display:flex;min-width:0;flex:1 1 0%;flex-direction:column;gap:var(--space-sm)}#playerModal#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__meta{display:block}#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__meta{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem;font-size:var(--font-size-xs);line-height:var(--line-height-tight);color:var(--text-muted-strong)}#playerModal#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__meta{display:block}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__meta{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem;font-size:var(--font-size-xs);line-height:var(--line-height-tight);color:var(--text-muted-strong)}#playerModal .video-modal__similar .player-modal__module-list{gap:clamp(var(--space-2xs),.85vw,var(--space-sm))}.player-modal__similar-card{outline:2px solid transparent;outline-offset:2px}.player-modal__similar-card:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}#playerModal.player-modal__similar-card{display:block}.player-modal__similar-card{position:relative;displayLine truncated
)}}.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;min-width:0;min-height:0}#playerModal.video-modal__secondary{display:block}.video-modal__secondary{display:flex;flex-direction:column;gap:1rem;min-width:0;min-height:0;width:100%;align-self:stretch}.video-modal__similar{display:none}@media (min-width:1024px){#playerModal.video-modal__similar{display:block}.video-modal__similar{display:flex;flex-direction:column;gap:1.5rem;align-self:stretch;flex:1 1 auto;min-height:0}#playerModal .video-modal__similar .player-modal__module{height:100%;display:flex;flex:1 1 auto;min-height:0;flex-direction:column}#playerModal .video-modal__similar .player-modal__module-list{flex:1 1 auto;min-height:0}#playerModal .video-modal__similar .player-modal__module-scroll{height:100%;max-height:none;min-height:0}}#playerModal .player-modal__module-item{width:100%}#playerModal#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card){display:block}#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card){display:flex;width:100%;flex-direction:row;flex-wrap:nowrap;align-items:stretch;gap:var(--space-md)}#playerModal#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card){display:block}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card){display:flex;width:100%;flex-direction:row;flex-wrap:nowrap;align-items:stretch;gap:var(--space-md)}#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__media{width:clamp(var(--video-modal-item-media-min),18vw,var(--video-modal-item-media-max))}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__media{width:clamp(var(--video-modal-item-media-min),18vw,var(--video-modal-item-media-max))!important}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__media,#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__media{flex-shrink:0;overflow:hidden;border-radius:var(--radius-lg)}#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__content{padding:clamp(var(--space-sm),1.25vw,var(--space-md))}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__content{padding:clamp(var(--space-sm),1.25vw,var(--space-md))!important}#playerModal#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__content{display:block}#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__content{display:flex;min-width:0;flex:1 1 0%;flex-direction:column;gap:var(--space-sm)}#playerModal#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__content{display:block}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__content{display:flex;min-width:0;flex:1 1 0%;flex-direction:column;gap:var(--space-sm)}#playerModal#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__meta{display:block}#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__meta{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem;font-size:var(--font-size-xs);line-height:var(--line-height-tight);color:var(--text-muted-strong)}#playerModal#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__meta{display:block}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__meta{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem;font-size:var(--font-size-xs);line-height:var(--line-height-tight);color:var(--text-muted-strong)}#playerModal .video-modal__similar .player-modal__module-list{gap:clamp(var(--space-2xs),.85vw,var(--space-sm))}.player-modal__similar-card{outline:2px solid transparent;outline-offset:2px}.player-modal__similar-card:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}#playerModal.player-modal__similar-card{display:block}.player-modal__similar-card{position:relative;displayLine truncated
--motion-video-card-backdrop-translate
);--video-card-backdrop-scale:1.12;--video-card-backdrop-blur:var(--blur-video-card-backdrop-strong);--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.watch-history-card:focus-visible{border-color:var(--color-accent);--tw-border-opacity:0.4;--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.watch-history-card[data-state=critical]{border-width:1px;border-color:var(--color-critical);--tw-border-opacity:0.4;background-color:var(--surface-raised-critical)}.watch-history-card[data-state=private]{background-color:var(--surface-raised-private);--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg)}.watch-history-card[data-state=disabled],.watch-history-card[data-state=private]{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.watch-history-card[data-state=disabled]{opacity:.6;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000}.watch-history-card[data-alert=nsfw-owner]{border-width:1px;border-color:var(--color-critical);--tw-border-opacity:0.4;background-color:var(--color-black)}.watch-history-card[data-alert=nsfw-owner]:hover{--tw-border-opacity:0.6;--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.watch-history-card img[data-thumbnail-state=blurred],.watch-history-card img[data-visual-state=blurred]{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(.4,0,.2,1);filter:blur(var(--blur-thumbnail-blur));transform:scale(1.05)}.watch-history-card[data-owner-is-viewer=true][data-url-health-state=offline][data-stream-health-state=unhealthy]{border-width:1px;border-color:var(--color-critical);--tw-border-opacity:0.4;background-color:var(--surface-raised-critical)}#playerModal#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card){display:block}#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card){display:flex;width:100%;flex-direction:row;flex-wrap:nowrap;align-items:stretch;gap:var(--space-md)}#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__media{width:clamp(var(--video-modal-item-media-min),18vw,var(--video-modal-item-media-max));flex-shrink:0;overflow:hidden;border-radius:var(--radius-lg)}#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__content{padding:clamp(var(--space-sm),1.25vw,var(--space-md))}#playerModal#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__content{display:block}#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__content{display:flex;min-width:0;flex:1 1 0%;flex-direction:column;gap:var(--space-sm)}#playerModal#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__meta{display:block}#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__meta{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem;font-size:var(--font-size-xs);line-height:var(--line-height-tight);color:var(--text-muted-strong)}.watch-history-card[data-motion=enter]{opacity:0;animation:card-motion-enter .22s ease-out forwards}#playerModal.watch-history-card{display:block}.watch-history-card{display:flex;flex-wrap:wrap;align-items:stretch;gap:1.25rem;padding:1rem;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)}@media (prefers-reduced-motion:reduce){.watch-history-card,.watch-history-card[data-motion=enter]{transition-property:none}.watch-history-card[data-motion=enter]{opacity:1;animation:none}}.watch-history-card:hover{--tw-translate-y:-0.125rem;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));--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}@media (min-width:640px){.watch-history-card{gap:1.5rem;padding:1.25rem}}.watch-history-card--private{border-color:var(--color-info);--tw-border-opacity:0.6}#playerModal.watch-histoLine truncated
+71 -42
View File
@@ -846,13 +846,21 @@
.moderation-badge__icon {
@apply inline-flex h-5 w-5 items-center justify-center rounded-full bg-status-warning-surface text-status-warning-on;
background-color: color-mix(in srgb, var(--status-warning) 18%, transparent);
background-color: color-mix(
in srgb,
var(--status-warning) 18%,
transparent
);
color: var(--status-warning-on);
}
.moderation-badge[data-variant="neutral"] .moderation-badge__icon {
@apply bg-status-neutral-surface text-status-neutral-on;
background-color: color-mix(in srgb, var(--status-neutral) 18%, transparent);
background-color: color-mix(
in srgb,
var(--status-neutral) 18%,
transparent
);
color: var(--status-neutral-on);
}
@@ -861,40 +869,65 @@
}
.moderation-badge__text {
@apply text-xs font-semibold tracking-tight whitespace-nowrap;
@apply whitespace-nowrap text-xs font-semibold tracking-tight;
}
.moderation-badge__action {
@apply inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-semibold normal-case tracking-tight transition duration-150 ease-out bg-status-warning-surface border-status-warning-border text-status-warning-on;
background-color: color-mix(in srgb, var(--status-warning) 16%, transparent);
border-color: color-mix(in srgb, var(--status-warning-border) 80%, transparent);
@apply inline-flex items-center gap-1.5 rounded-full border border-status-warning-border bg-status-warning-surface px-3 py-1 text-xs font-semibold normal-case tracking-tight text-status-warning-on transition duration-150 ease-out;
background-color: color-mix(
in srgb,
var(--status-warning) 16%,
transparent
);
border-color: color-mix(
in srgb,
var(--status-warning-border) 80%,
transparent
);
color: var(--status-warning-on);
}
.moderation-badge[data-variant="warning"] .moderation-badge__action:hover {
background-color: color-mix(in srgb, var(--status-warning) 24%, transparent);
background-color: color-mix(
in srgb,
var(--status-warning) 24%,
transparent
);
}
.moderation-badge[data-variant="neutral"] .moderation-badge__action {
@apply bg-status-neutral-surface border-status-neutral-border text-status-neutral-on;
background-color: color-mix(in srgb, var(--status-neutral) 16%, transparent);
border-color: color-mix(in srgb, var(--status-neutral-border) 80%, transparent);
@apply border-status-neutral-border bg-status-neutral-surface text-status-neutral-on;
background-color: color-mix(
in srgb,
var(--status-neutral) 16%,
transparent
);
border-color: color-mix(
in srgb,
var(--status-neutral-border) 80%,
transparent
);
color: var(--status-neutral-on);
}
.moderation-badge[data-variant="neutral"] .moderation-badge__action:hover {
background-color: color-mix(in srgb, var(--status-neutral) 24%, transparent);
background-color: color-mix(
in srgb,
var(--status-neutral) 24%,
transparent
);
}
.moderation-badge__action:focus-visible {
outline: var(--outline-thick-width)
solid color-mix(in srgb, var(--status-warning) 38%, transparent);
outline: var(--outline-thick-width) solid
color-mix(in srgb, var(--status-warning) 38%, transparent);
outline-offset: var(--space-3xs);
}
.moderation-badge[data-variant="neutral"] .moderation-badge__action:focus-visible {
outline: var(--outline-thick-width)
solid color-mix(in srgb, var(--status-neutral) 38%, transparent);
.moderation-badge[data-variant="neutral"]
.moderation-badge__action:focus-visible {
outline: var(--outline-thick-width) solid
color-mix(in srgb, var(--status-neutral) 38%, transparent);
}
.moderation-badge__action[disabled] {
@@ -1082,7 +1115,9 @@
.card:focus-within {
--video-card-lift-translate-y: var(--motion-video-card-hover-translate);
--video-card-backdrop-opacity: 0.425;
--video-card-backdrop-translate-y: var(--motion-video-card-backdrop-translate);
--video-card-backdrop-translate-y: var(
--motion-video-card-backdrop-translate
);
--video-card-backdrop-scale: 1.12;
--video-card-backdrop-blur: var(--blur-video-card-backdrop-strong);
@apply shadow-lg;
@@ -1493,7 +1528,9 @@
position: relative;
}
#playerModal .video-modal__video[data-visual-state="blurred"] .video-modal__media {
#playerModal
.video-modal__video[data-visual-state="blurred"]
.video-modal__media {
@apply transition duration-base ease-in-out;
filter: blur(var(--blur-thumbnail-blur));
transform: scale(1.05);
@@ -2076,9 +2113,9 @@
width: 100%;
}
#playerModal .player-modal__module-item > .card:not(
.player-modal__similar-card
) {
#playerModal
.player-modal__module-item
> .card:not(.player-modal__similar-card) {
@apply flex w-full flex-row flex-nowrap items-stretch gap-md;
}
@@ -2164,7 +2201,7 @@
}
.player-modal__similar-card-author {
@apply flex items-center min-w-0;
@apply flex min-w-0 items-center;
}
.player-modal__similar-card-author-meta {
@@ -2398,7 +2435,7 @@
}
.video-modal__actions-row {
@apply flex w-full flex-wrap items-start gap-y-3 gap-x-3 sm:gap-x-4;
@apply flex w-full flex-wrap items-start gap-x-3 gap-y-3 sm:gap-x-4;
}
.video-modal__reactions,
@@ -2571,11 +2608,8 @@
margin: 0;
padding: var(--space-md);
border-radius: var(--radius-lg);
border: var(--border-width-hairline) dashed color-mix(
in srgb,
var(--border-default) 40%,
transparent
);
border: var(--border-width-hairline) dashed
color-mix(in srgb, var(--border-default) 40%, transparent);
background-color: var(--surface-alt);
}
@@ -2680,21 +2714,14 @@
.comment-thread__load-more:focus-visible {
border-color: var(--border-default);
color: var(--text-primary);
background-color: color-mix(
in srgb,
var(--surface-alt) 70%,
transparent
);
background-color: color-mix(in srgb, var(--surface-alt) 70%, transparent);
}
.comment-thread__disabled {
@apply text-sm text-muted;
border-radius: var(--radius-lg);
border: var(--border-width-hairline) dashed color-mix(
in srgb,
var(--border-default) 32%,
transparent
);
border: var(--border-width-hairline) dashed
color-mix(in srgb, var(--border-default) 32%, transparent);
background-color: var(--surface-alt);
padding: var(--space-md);
}
@@ -3243,7 +3270,7 @@
}
.watch-history-card__thumbnail {
@apply block flex-none text-text no-underline w-56 max-w-full;
@apply block w-56 max-w-full flex-none text-text no-underline;
}
.watch-history-card__thumbnailInner {
@@ -3352,7 +3379,9 @@
--text-status-warning-on: var(--color-warning-on);
}
#playerModal .video-modal__moderation-bar .moderation-badge[data-variant="neutral"] {
#playerModal
.video-modal__moderation-bar
.moderation-badge[data-variant="neutral"] {
color: var(--color-text);
}
}
@@ -3716,8 +3745,8 @@
flex-direction: column;
align-items: center;
gap: var(--space-md);
padding: calc(var(--space-xl) + env(safe-area-inset-top))
var(--space-lg) var(--space-lg);
padding: calc(var(--space-xl) + env(safe-area-inset-top)) var(--space-lg)
var(--space-lg);
pointer-events: none;
opacity: 0;
transform: translateY(var(--toast-translate));
+9 -5
View File
@@ -15,7 +15,11 @@
<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">
<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>
@@ -343,11 +347,11 @@
</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">
<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>
<button class="modal-primary sm:w-auto">Publish</button>
</div>
</div>
</div>
+116 -63
View File
@@ -50,9 +50,9 @@
<main id="moderationFixtureRoot" class="moderation-fixtures__main">
<h1 class="moderation-fixtures__title">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.
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">
@@ -74,94 +74,95 @@
id: "1".repeat(64),
title: "Fixture: Blurred Preview",
pubkey: "a".repeat(64),
thumbnail: "/assets/jpg/video-thumbnail-fallback.jpg",
thumbnail: "/assets/jpg/video-thumbnail-fallback.jpg"
},
moderation: {
blockAutoplay: true,
blurThumbnail: true,
trustedCount: 3,
reportType: "nudity",
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 },
{ name: "Omar F1", pubkey: "d".repeat(64), latest: 1_700_000_030 }
],
blockToggle: {
label: "Simulate viewer blocking Mira F1",
pubkey: "c".repeat(64),
},
pubkey: "c".repeat(64)
}
},
{
key: "autoplay-threshold",
heading: "Autoplay threshold (config default: ≥2 trusted nudity reports)",
heading:
"Autoplay threshold (config default: ≥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",
thumbnail: "/assets/jpg/video-thumbnail-fallback.jpg"
},
moderation: {
blockAutoplay: true,
blurThumbnail: false,
trustedCount: 2,
reportType: "nudity",
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: "Mira F1", pubkey: "c".repeat(64), latest: 1_700_000_020 }
]
},
{
key: "show-anyway",
heading: "\"Show anyway\" override (persists locally)",
heading: '"Show anyway" override (persists locally)',
description:
"Card starts blurred and blocked; activating \"Show anyway\" should persist across reloads.",
'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",
thumbnail: "/assets/jpg/video-thumbnail-fallback.jpg"
},
moderation: {
blockAutoplay: true,
blurThumbnail: true,
trustedCount: 3,
reportType: "nudity",
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 },
],
{ name: "Omar F1", pubkey: "d".repeat(64), latest: 1_700_000_030 }
]
},
{
key: "trusted-report-hide",
heading: "Trusted report hide (config default: ≥3 spam reports)",
description:
"Card should render hidden with spam copy until the viewer clicks \"Show anyway\".",
'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",
thumbnail: "/assets/jpg/video-thumbnail-fallback.jpg"
},
moderation: {
blockAutoplay: true,
blurThumbnail: true,
trustedCount: 3,
reportType: "spam",
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 },
],
{ name: "Omar F1", pubkey: "d".repeat(64), latest: 1_700_000_030 }
]
},
{
key: "trusted-mute-hide",
@@ -174,16 +175,16 @@
id: "5".repeat(64),
title: "Fixture: Trusted Mute Hide",
pubkey: "bb".repeat(32),
thumbnail: "/assets/jpg/video-thumbnail-fallback.jpg",
thumbnail: "/assets/jpg/video-thumbnail-fallback.jpg"
},
moderation: {
blockAutoplay: false,
blurThumbnail: false,
trustedMuted: true,
trustedMuteCount: 1,
},
muters: [{ name: "Jess F1", pubkey: "b".repeat(64) }],
trustedMuteCount: 1
},
muters: [{ name: "Jess F1", pubkey: "b".repeat(64) }]
}
];
const blockedReportersByVideo = new Map();
@@ -196,7 +197,12 @@
}
function setReporterBlocked(videoId, pubkey, blocked) {
if (typeof videoId !== "string" || !videoId || typeof pubkey !== "string" || !pubkey) {
if (
typeof videoId !== "string" ||
!videoId ||
typeof pubkey !== "string" ||
!pubkey
) {
return;
}
@@ -229,7 +235,9 @@
}
function getActiveReporters(fixture, videoId) {
const reporters = Array.isArray(fixture?.reporters) ? fixture.reporters : [];
const reporters = Array.isArray(fixture?.reporters)
? fixture.reporters
: [];
if (!reporters.length) {
return [];
}
@@ -240,7 +248,8 @@
}
return reporters.filter((reporter) => {
const key = typeof reporter?.pubkey === "string" ? reporter.pubkey : "";
const key =
typeof reporter?.pubkey === "string" ? reporter.pubkey : "";
if (!key) {
return false;
}
@@ -258,11 +267,14 @@
if (!reporter || typeof reporter !== "object") {
return null;
}
const pubkey = typeof reporter.pubkey === "string" ? reporter.pubkey : "";
const pubkey =
typeof reporter.pubkey === "string" ? reporter.pubkey : "";
if (!pubkey) {
return null;
}
const latest = Number.isFinite(reporter.latest) ? Math.floor(reporter.latest) : 0;
const latest = Number.isFinite(reporter.latest)
? Math.floor(reporter.latest)
: 0;
const name = typeof reporter.name === "string" ? reporter.name : "";
return { name, pubkey, latest };
})
@@ -302,7 +314,10 @@
? sanitizedReporters
: normalizeFixtureReporters(getActiveReporters(fixture, videoId));
const latest = reporters.reduce((max, reporter) => Math.max(max, reporter.latest), 0);
const latest = reporters.reduce(
(max, reporter) => Math.max(max, reporter.latest),
0
);
return {
eventId: fixture.video.id,
@@ -311,10 +326,10 @@
[fixture.moderation.reportType]: {
trusted: reporters.length,
total: reporters.length,
latest,
latest
}
},
},
updatedAt: Date.now(),
updatedAt: Date.now()
};
}
@@ -324,7 +339,13 @@
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,
...fixture.thresholds
}
: { blur: 3, autoplay: 2, reportHide: 3, muteHide: 1 };
const autoplayThreshold = Number.isFinite(thresholds.autoplay)
? Math.max(0, Math.floor(thresholds.autoplay))
@@ -346,16 +367,22 @@
.map((reporter) => reporter.name)
.filter((value) => typeof value === "string" && value);
const reporterPubkeys = sanitizedReporters.map((reporter) => reporter.pubkey);
const reporterPubkeys = sanitizedReporters.map(
(reporter) => reporter.pubkey
);
const trustedReporterEntries = sanitizedReporters.map((reporter) => ({
pubkey: reporter.pubkey,
latest: reporter.latest,
latest: reporter.latest
}));
const muterEntries = Array.isArray(fixture.muters) ? fixture.muters : [];
const muterEntries = Array.isArray(fixture.muters)
? fixture.muters
: [];
const muterPubkeys = muterEntries
.map((muter) => (typeof muter?.pubkey === "string" ? muter.pubkey : ""))
.map((muter) =>
typeof muter?.pubkey === "string" ? muter.pubkey : ""
)
.filter(Boolean);
const muterDisplayNames = muterEntries
.map((muter) => (typeof muter?.name === "string" ? muter.name : ""))
@@ -390,7 +417,7 @@
const hideCounts = hidden
? {
trustedMuteCount: muteCount,
trustedReportCount: trustedCount,
trustedReportCount: trustedCount
}
: null;
@@ -403,7 +430,7 @@
trustedMuted,
trustedMuters: muterPubkeys,
trustedMuteCount: muteCount,
trustedMuterDisplayNames: muterDisplayNames,
trustedMuterDisplayNames: muterDisplayNames
};
const summary = buildSummary(fixture, videoId, sanitizedReporters);
@@ -425,12 +452,18 @@
hideReason: hidden ? hideReason : "",
hideCounts: hideCounts
? {
trustedMuteCount: Math.max(0, Number(hideCounts.trustedMuteCount) || 0),
trustedReportCount: Math.max(0, Number(hideCounts.trustedReportCount) || 0),
trustedMuteCount: Math.max(
0,
Number(hideCounts.trustedMuteCount) || 0
),
trustedReportCount: Math.max(
0,
Number(hideCounts.trustedReportCount) || 0
)
}
: null,
hideBypass,
hideTriggered: hidden,
hideTriggered: hidden
};
base.original = {
@@ -440,7 +473,7 @@
hideReason: originalState.hideReason,
hideCounts: originalState.hideCounts,
hideBypass: originalState.hideBypass,
hideTriggered: originalState.hideTriggered,
hideTriggered: originalState.hideTriggered
};
if (overrideEntry?.showAnyway) {
@@ -448,7 +481,7 @@
showAnyway: true,
updatedAt: Number.isFinite(overrideEntry.updatedAt)
? Math.floor(overrideEntry.updatedAt)
: Date.now(),
: Date.now()
};
base.blockAutoplay = false;
base.blurThumbnail = false;
@@ -501,7 +534,7 @@
...entry,
updatedAt: Number.isFinite(entry?.updatedAt)
? Math.floor(entry.updatedAt)
: Date.now(),
: Date.now()
};
persistOverrides(next);
return next;
@@ -509,7 +542,7 @@
function createCard(fixture, overrides) {
const video = {
...fixture.video,
...fixture.video
};
video.moderation = buildModerationState(fixture, overrides, video.id);
@@ -524,7 +557,7 @@
return value.toLocaleString("en-US");
}
return String(value ?? "");
},
}
},
helpers: {
isMagnetSupported: () => true,
@@ -534,19 +567,23 @@
return value.toLocaleString(locales || "en-US", options);
}
return String(value ?? "");
},
}
},
assets: {
fallbackThumbnailSrc: "/assets/jpg/video-thumbnail-fallback.jpg",
fallbackThumbnailSrc: "/assets/jpg/video-thumbnail-fallback.jpg"
},
designSystem: {
getMode: () => "new",
isNew: () => true,
},
isNew: () => true
}
});
const refreshModeration = () => {
card.video.moderation = buildModerationState(fixture, overrides, video.id);
card.video.moderation = buildModerationState(
fixture,
overrides,
video.id
);
if (typeof card.refreshModerationUi === "function") {
card.refreshModerationUi();
}
@@ -585,11 +622,16 @@
if (fixture.description) {
const description = document.createElement("p");
description.textContent = fixture.description;
description.classList.add("moderation-fixtures__section-description");
description.classList.add(
"moderation-fixtures__section-description"
);
section.appendChild(description);
}
const { root: cardRoot, refreshModeration } = createCard(fixture, overrides);
const { root: cardRoot, refreshModeration } = createCard(
fixture,
overrides
);
section.appendChild(cardRoot);
if (fixture.blockToggle && fixture.blockToggle.pubkey) {
@@ -601,14 +643,25 @@
const input = document.createElement("input");
input.type = "checkbox";
input.checked = isReporterBlocked(fixture.video.id, fixture.blockToggle.pubkey);
input.checked = isReporterBlocked(
fixture.video.id,
fixture.blockToggle.pubkey
);
input.addEventListener("change", () => {
const checked = input.checked;
setReporterBlocked(fixture.video.id, fixture.blockToggle.pubkey, 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);
const detail = {
action,
targetPubkey: fixture.blockToggle.pubkey
};
const maybePromise =
moderationService?.queueUserBlockRefresh?.(detail);
if (maybePromise && typeof maybePromise.then === "function") {
maybePromise.catch(() => {});
}
+35 -16
View File
@@ -11,9 +11,9 @@
<header class="space-y-sm">
<h1 class="text-2xl font-semibold">Popover positioning scenarios</h1>
<p class="max-w-3xl text-sm text-muted">
Interactive harness that exercises the popover engine near the edges of the
viewport, inside modals, and within scroll containers. Playwright scenarios
rely on the data attributes surfaced here.
Interactive harness that exercises the popover engine near the edges
of the viewport, inside modals, and within scroll containers.
Playwright scenarios rely on the data attributes surfaced here.
</p>
</header>
@@ -21,13 +21,16 @@
<div class="space-y-xs">
<h2 class="text-xl font-semibold">Grid edge cases</h2>
<p class="text-sm text-muted">
The rightmost card hugs the viewport edge to verify collision handling.
The rightmost card hugs the viewport edge to verify collision
handling.
</p>
</div>
<div class="grid gap-lg sm:grid-cols-2 lg:grid-cols-3">
<article class="card space-y-sm p-md">
<h3 class="text-lg font-semibold">Top left</h3>
<p class="text-sm text-muted">Baseline placement without collisions.</p>
<p class="text-sm text-muted">
Baseline placement without collisions.
</p>
<button
class="btn-ghost"
type="button"
@@ -37,7 +40,10 @@
Menu
</button>
</article>
<article class="card space-y-sm p-md" data-test-card="grid-bottom-right">
<article
class="card space-y-sm p-md"
data-test-card="grid-bottom-right"
>
<h3 class="text-lg font-semibold">Bottom right</h3>
<p class="text-sm text-muted">
Forces the panel to flip when it would overflow the viewport.
@@ -55,7 +61,9 @@
</article>
<article class="card space-y-sm p-md">
<h3 class="text-lg font-semibold">Center</h3>
<p class="text-sm text-muted">Acts as a control for viewport padding.</p>
<p class="text-sm text-muted">
Acts as a control for viewport padding.
</p>
<div class="flex justify-center">
<button
class="btn-ghost"
@@ -74,23 +82,28 @@
<div class="space-y-xs">
<h2 class="text-xl font-semibold">Video modal integration</h2>
<p class="text-sm text-muted">
Mimics the production video modal so we can verify popovers respect the overlay root.
Mimics the production video modal so we can verify popovers respect
the overlay root.
</p>
</div>
<button class="btn w-40" type="button" data-test-open-modal>
Open modal
</button>
<div
class="fixed inset-0 z-overlay-modal hidden items-center justify-center bg-overlay/60 backdrop-blur"
class="z-overlay-modal bg-overlay/60 fixed inset-0 hidden items-center justify-center backdrop-blur"
role="dialog"
aria-modal="true"
aria-labelledby="testModalTitle"
data-test-modal
>
<div class="bv-modal__panel flex max-h-modal-shell flex-col gap-md p-lg">
<div
class="bv-modal__panel flex max-h-modal-shell flex-col gap-md p-lg"
>
<header class="flex items-center justify-between gap-sm">
<div>
<p class="text-xs uppercase tracking-wide text-muted">Now playing</p>
<p class="text-xs uppercase tracking-wide text-muted">
Now playing
</p>
<h3 id="testModalTitle" class="text-lg font-semibold">
Edge-of-viewport sample
</h3>
@@ -101,7 +114,8 @@
</header>
<div class="space-y-md">
<p class="text-sm text-muted">
Interact with the more menu to verify popovers inside scroll-locked contexts.
Interact with the more menu to verify popovers inside
scroll-locked contexts.
</p>
<div class="flex justify-end">
<button
@@ -122,14 +136,15 @@
<div class="space-y-xs">
<h2 class="text-xl font-semibold">Scroll container</h2>
<p class="text-sm text-muted">
Ensures auto-update keeps popovers aligned when the trigger lives in overflow regions.
Ensures auto-update keeps popovers aligned when the trigger lives in
overflow regions.
</p>
</div>
<div
class="h-64 overflow-auto rounded-xl border border-border bg-surface-alt p-md"
data-test-scroll-region
>
<div class="space-y-lg docs-popover-scenarios__scroll-spacer">
<div class="docs-popover-scenarios__scroll-spacer space-y-lg">
<p class="text-sm text-muted">
Scroll to the bottom to find the trigger used in automated tests.
</p>
@@ -155,7 +170,8 @@
function buildMenu({ container, trigger, id }) {
const panel = container.ownerDocument.createElement("div");
panel.className = "popover__panel card w-64 max-w-popover-safe space-y-sm";
panel.className =
"popover__panel card w-64 max-w-popover-safe space-y-sm";
panel.dataset.testPanel = id;
const heading = container.ownerDocument.createElement("h4");
@@ -198,7 +214,10 @@
const triggers = [
{ selector: "[data-test-trigger=grid-top-left]", id: "grid-top-left" },
{ selector: "[data-test-trigger=grid-bottom-right]", id: "grid-bottom-right" },
{
selector: "[data-test-trigger=grid-bottom-right]",
id: "grid-bottom-right"
},
{ selector: "[data-test-trigger=grid-center]", id: "grid-center" },
{ selector: "[data-test-trigger=modal-menu]", id: "modal-menu" },
{ selector: "[data-test-trigger=scroll-bottom]", id: "scroll-bottom" }
+38 -23
View File
@@ -14,7 +14,7 @@ module.exports = {
extend: {
screens: {
xs: "30rem",
compact: "25rem",
compact: "25rem"
},
colors: {
page: "var(--color-page)",
@@ -29,7 +29,7 @@ module.exports = {
"panel-hover": "var(--surface-panel-hover)",
sunken: "var(--surface-sunken)",
muted: "var(--surface-muted)",
inverse: "var(--surface-inverse)",
inverse: "var(--surface-inverse)"
},
overlay: {
DEFAULT: "var(--surface-overlay)",
@@ -202,7 +202,7 @@ module.exports = {
borderColor: {
surface: "var(--border-default)",
"surface-strong": "var(--border-strong)",
"overlay": "var(--border-overlay)",
overlay: "var(--border-overlay)",
"overlay-strong": "var(--border-overlay-strong)",
"status-info-border": "var(--status-info-border)",
"status-success-border": "var(--status-success-border)",
@@ -278,16 +278,22 @@ module.exports = {
"sidebar-shell-block-end": "var(--space-sidebar-shell-block-end)",
"sidebar-scroll-gap": "var(--space-sidebar-scroll-gap)",
"sidebar-panel-gap": "var(--space-sidebar-panel-gap)",
"sidebar-panel-padding-block": "var(--space-sidebar-panel-padding-block)",
"sidebar-panel-padding-inline": "var(--space-sidebar-panel-padding-inline)",
"sidebar-panel-padding-block":
"var(--space-sidebar-panel-padding-block)",
"sidebar-panel-padding-inline":
"var(--space-sidebar-panel-padding-inline)",
"sidebar-footer-gap": "var(--space-sidebar-footer-gap)",
"sidebar-trigger-padding-block": "var(--space-sidebar-trigger-padding-block)",
"sidebar-trigger-padding-block":
"var(--space-sidebar-trigger-padding-block)",
"sidebar-popover-offset": "var(--space-sidebar-popover-offset)",
"sidebar-popover-padding-block": "var(--space-sidebar-popover-padding-block)",
"sidebar-popover-padding-inline": "var(--space-sidebar-popover-padding-inline)",
"sidebar-popover-padding-block":
"var(--space-sidebar-popover-padding-block)",
"sidebar-popover-padding-inline":
"var(--space-sidebar-popover-padding-inline)",
"sidebar-popover-gap": "var(--space-sidebar-popover-gap)",
"sidebar-nav-padding-block": "var(--space-sidebar-nav-padding-block)",
"sidebar-subnav-padding-block": "var(--space-sidebar-subnav-padding-block)",
"sidebar-subnav-padding-block":
"var(--space-sidebar-subnav-padding-block)",
"sidebar-mobile-gutter": "var(--sidebar-mobile-gutter)",
"popup-inline": "var(--popup-padding-inline)",
"popup-block": "var(--popup-padding-block)",
@@ -322,9 +328,18 @@ module.exports = {
mono: ["var(--font-family-mono)"]
},
fontSize: {
"4xs": ["var(--font-size-4xs)", { lineHeight: "var(--line-height-tight)" }],
"3xs": ["var(--font-size-3xs)", { lineHeight: "var(--line-height-tight)" }],
"2xs": ["var(--font-size-2xs)", { lineHeight: "var(--line-height-tight)" }],
"4xs": [
"var(--font-size-4xs)",
{ lineHeight: "var(--line-height-tight)" }
],
"3xs": [
"var(--font-size-3xs)",
{ lineHeight: "var(--line-height-tight)" }
],
"2xs": [
"var(--font-size-2xs)",
{ lineHeight: "var(--line-height-tight)" }
],
xs: ["var(--font-size-xs)", { lineHeight: "var(--line-height-tight)" }],
sm: ["var(--font-size-sm)", { lineHeight: "var(--line-height-snug)" }],
base: [
@@ -379,19 +394,19 @@ module.exports = {
lineHeight: "1.6",
maxWidth: "none",
fontFamily: theme("fontFamily.sans").join(", "),
"h1": {
h1: {
fontWeight: theme("fontWeight.bold"),
marginTop: "0.5rem",
marginBottom: "1rem",
fontSize: theme("fontSize.2xl")[0]
},
"h2": {
h2: {
fontWeight: theme("fontWeight.bold"),
marginTop: "1.25rem",
marginBottom: "0.75rem",
fontSize: theme("fontSize.xl")[0]
},
"h3": {
h3: {
fontWeight: theme("fontWeight.semibold"),
marginTop: "1rem",
marginBottom: "0.75rem",
@@ -402,7 +417,7 @@ module.exports = {
marginTop: "0.75rem",
marginBottom: "0.5rem"
},
"p": {
p: {
marginTop: "0",
marginBottom: "1rem"
},
@@ -411,11 +426,11 @@ module.exports = {
marginBottom: "1rem",
paddingLeft: "1.25rem"
},
"li": {
li: {
marginTop: "0.5rem",
marginBottom: "0.5rem"
},
"blockquote": {
blockquote: {
borderLeftWidth: "4px",
borderLeftColor: "var(--color-border-subtle)",
paddingLeft: "1rem",
@@ -423,7 +438,7 @@ module.exports = {
marginBottom: "1rem",
color: "var(--color-muted)"
},
"code": {
code: {
backgroundColor: "var(--color-page-alt)",
color: "var(--color-secondary)",
padding: "0.2rem 0.4rem",
@@ -438,7 +453,7 @@ module.exports = {
"code::after": {
content: "none"
},
"pre": {
pre: {
backgroundColor: "var(--color-page-alt)",
color: "var(--color-text-strong)",
padding: "1rem",
@@ -456,7 +471,7 @@ module.exports = {
fontWeight: "inherit",
color: "inherit"
},
"a": {
a: {
color: "var(--color-primary)",
textDecoration: "none",
fontWeight: theme("fontWeight.medium")
@@ -464,13 +479,13 @@ module.exports = {
"a:hover": {
textDecoration: "underline"
},
"hr": {
hr: {
borderColor: "var(--color-border-subtle)",
borderTopWidth: "1px",
marginTop: "2rem",
marginBottom: "2rem"
},
"img": {
img: {
marginTop: "0",
marginBottom: "1rem",
borderRadius: theme("borderRadius.md"),