mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-12 06:48:55 +00:00
7264c44fd7
app.blade.php is the root template for all three interfaces, and it opened
with two lines pointing at a third party:
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=instrument-sans:400,500,600" rel="stylesheet" />
Every visitor to /login, /register, /forgot-password, /s/{token} and every
public listing page therefore made a request to a host the operator did
not choose and could not switch off, before they had done anything at all
-- handing it their IP address, their user agent, and through Origin the
hostname of the installation they were visiting. On the signed-out pages
that is a visitor who has agreed to nothing, and an operator who often has
told their own users that this server is where their files live.
There was no self-hosted copy in the repository, no setting, no mention in
INSTALL.md, DOCKER.md or SECURITY.md, and no SRI on the tag.
The font now ships with the application, through @fontsource/instrument-sans
-- the same font, the same three weights the URL asked for, from a
versioned dependency rather than binaries pasted into the repository.
Vite fingerprints and emits them like any other asset.
Cost, measured on this build: twelve files, 192 KB on disk. A browser
fetches only woff2 and only the subsets it needs, which is 73 KB for all
six woff2 files together and typically 41 KB (latin, three weights) for a
page in English. Against that, every page load loses a DNS lookup, a TLS
handshake and a round trip to another origin, so signed-out pages get
faster rather than slower.
This is a privacy change rather than a vulnerability fix, and worth saying
plainly: the share token does not leak this way. Referrer-Policy:
strict-origin-when-cross-origin is set in both nginx configs and in the
INSTALL.md snippet, so the path never travelled in the Referer. What
travelled was the visit itself.
Not changed: public/.htaccess still sets no security headers at all, so an
Apache installation has no Referrer-Policy. That is a real gap and a
separate change.
Verified: `npm run build` succeeds and emits the faces; no reference to
the CDN survives anywhere in public/build; `tsc --noEmit` and prettier are
clean. No test asserts on the font, before or after.
378 lines
10 KiB
CSS
378 lines
10 KiB
CSS
@import 'tailwindcss';
|
|
|
|
/*
|
|
* Instrument Sans, served by this installation rather than by a font CDN.
|
|
*
|
|
* The three weights the design uses, which is what the stylesheet this
|
|
* replaces asked for. Every visitor to /login, /register, /forgot-password
|
|
* and every public share link used to make a request to a third party
|
|
* before they had done anything, handing it their IP, their user agent and
|
|
* the hostname of the installation they were visiting — on the signed-out
|
|
* pages, where the visitor has not agreed to anything and the operator
|
|
* never chose the recipient. Bundling it makes those pages talk only to
|
|
* the server they were asked for.
|
|
*/
|
|
@import '@fontsource/instrument-sans/400.css';
|
|
@import '@fontsource/instrument-sans/500.css';
|
|
@import '@fontsource/instrument-sans/600.css';
|
|
|
|
@plugin 'tailwindcss-animate';
|
|
|
|
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
|
|
|
/*
|
|
* Companion packages under packages/ (e.g. packages/cloud-modules) live
|
|
* under a gitignored directory, which Tailwind v4's default content
|
|
* detection skips entirely — without this, any utility class used only in
|
|
* a package page (never duplicated in core) silently never gets generated.
|
|
*/
|
|
@source '../../packages/*/resources/js/**/*.tsx';
|
|
|
|
@custom-variant dark (&:is(.dark *));
|
|
|
|
@theme {
|
|
--font-sans:
|
|
'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
|
|
|
--radius-lg: var(--radius);
|
|
--radius-md: calc(var(--radius) - 2px);
|
|
--radius-sm: calc(var(--radius) - 4px);
|
|
|
|
--color-background: var(--background);
|
|
--color-foreground: var(--foreground);
|
|
|
|
--color-card: var(--card);
|
|
--color-card-foreground: var(--card-foreground);
|
|
|
|
--color-popover: var(--popover);
|
|
--color-popover-foreground: var(--popover-foreground);
|
|
|
|
--color-primary: var(--primary);
|
|
--color-primary-foreground: var(--primary-foreground);
|
|
|
|
--color-secondary: var(--secondary);
|
|
--color-secondary-foreground: var(--secondary-foreground);
|
|
|
|
--color-muted: var(--muted);
|
|
--color-muted-foreground: var(--muted-foreground);
|
|
|
|
--color-accent: var(--accent);
|
|
--color-accent-foreground: var(--accent-foreground);
|
|
|
|
--color-destructive: var(--destructive);
|
|
--color-destructive-foreground: var(--destructive-foreground);
|
|
|
|
--color-success: var(--success);
|
|
--color-success-foreground: var(--success-foreground);
|
|
|
|
--color-border: var(--border);
|
|
--color-input: var(--input);
|
|
--color-ring: var(--ring);
|
|
|
|
--color-chart-1: var(--chart-1);
|
|
--color-chart-2: var(--chart-2);
|
|
--color-chart-3: var(--chart-3);
|
|
--color-chart-4: var(--chart-4);
|
|
--color-chart-5: var(--chart-5);
|
|
|
|
--color-sidebar: var(--sidebar-background);
|
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
--color-sidebar-primary: var(--sidebar-primary);
|
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
--color-sidebar-accent: var(--sidebar-accent);
|
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
--color-sidebar-border: var(--sidebar-border);
|
|
--color-sidebar-ring: var(--sidebar-ring);
|
|
}
|
|
|
|
/*
|
|
The default border color has changed to `currentColor` in Tailwind CSS v4,
|
|
so we've added these compatibility styles to make sure everything still
|
|
looks the same as it did with Tailwind CSS v3.
|
|
|
|
If we ever want to remove these styles, we need to add an explicit border
|
|
color utility to any element that depends on these defaults.
|
|
*/
|
|
@layer base {
|
|
*,
|
|
::after,
|
|
::before,
|
|
::backdrop,
|
|
::file-selector-button {
|
|
border-color: var(--color-gray-200, currentColor);
|
|
}
|
|
}
|
|
|
|
:root {
|
|
--background: hsl(0, 0%, 100%);
|
|
--foreground: hsl(0, 0%, 3.9%);
|
|
--card: hsl(0, 0%, 100%);
|
|
--card-foreground: hsl(0, 0%, 3.9%);
|
|
--popover: hsl(0, 0%, 100%);
|
|
--popover-foreground: hsl(0, 0%, 3.9%);
|
|
--primary: #4c2ab6; /* ProjectSend brand */
|
|
--primary-foreground: hsl(0, 0%, 98%);
|
|
--secondary: hsl(0, 0%, 96.1%);
|
|
--secondary-foreground: hsl(0, 0%, 9%);
|
|
--muted: hsl(0, 0%, 96.1%);
|
|
--muted-foreground: hsl(0, 0%, 45.1%);
|
|
--accent: hsl(255, 60%, 96%);
|
|
--accent-foreground: hsl(255, 50%, 30%);
|
|
--destructive: hsl(0, 84.2%, 60.2%);
|
|
--destructive-foreground: hsl(0, 0%, 98%);
|
|
--success: hsl(142, 71%, 45%);
|
|
--success-foreground: hsl(0, 0%, 98%);
|
|
--border: hsl(0, 0%, 92.8%);
|
|
--input: hsl(0, 0%, 89.8%);
|
|
--ring: #4c2ab6;
|
|
--chart-1: hsl(12, 76%, 61%);
|
|
--chart-2: hsl(173, 58%, 39%);
|
|
--chart-3: hsl(197, 37%, 24%);
|
|
--chart-4: hsl(43, 74%, 66%);
|
|
--chart-5: hsl(27, 87%, 67%);
|
|
--radius: 0.5rem;
|
|
--sidebar-background: hsl(0, 0%, 98%);
|
|
--sidebar-foreground: hsl(240, 5.3%, 30%);
|
|
--sidebar-primary: #4c2ab6;
|
|
--sidebar-primary-foreground: hsl(0, 0%, 98%);
|
|
--sidebar-accent: hsl(0, 0%, 94%);
|
|
--sidebar-accent-foreground: hsl(0, 0%, 30%);
|
|
--sidebar-border: hsl(0, 0%, 91%);
|
|
--sidebar-ring: #4c2ab6;
|
|
}
|
|
|
|
.dark {
|
|
--background: hsl(0, 0%, 9%);
|
|
--foreground: hsl(0, 0%, 98%);
|
|
--card: hsl(0, 0%, 12%);
|
|
--card-foreground: hsl(0, 0%, 98%);
|
|
--popover: hsl(0, 0%, 12%);
|
|
--popover-foreground: hsl(0, 0%, 98%);
|
|
--primary: hsl(255, 70%, 62%); /* ProjectSend brand, lightened for dark surfaces */
|
|
--primary-foreground: hsl(0, 0%, 98%);
|
|
--secondary: hsl(0, 0%, 17%);
|
|
--secondary-foreground: hsl(0, 0%, 98%);
|
|
--muted: hsl(0, 0%, 14%);
|
|
--muted-foreground: hsl(0, 0%, 65%);
|
|
--accent: hsl(255, 25%, 18%);
|
|
--accent-foreground: hsl(255, 60%, 92%);
|
|
--destructive: hsl(0, 84%, 60%);
|
|
--destructive-foreground: hsl(0, 0%, 98%);
|
|
--success: hsl(142, 65%, 42%);
|
|
--success-foreground: hsl(0, 0%, 98%);
|
|
--border: hsl(0, 0%, 18%);
|
|
--input: hsl(0, 0%, 18%);
|
|
--ring: hsl(255, 70%, 62%);
|
|
--chart-1: hsl(220, 70%, 50%);
|
|
--chart-2: hsl(160, 60%, 45%);
|
|
--chart-3: hsl(30, 80%, 55%);
|
|
--chart-4: hsl(280, 65%, 60%);
|
|
--chart-5: hsl(340, 75%, 55%);
|
|
--sidebar-background: hsl(0, 0%, 7%);
|
|
--sidebar-foreground: hsl(0, 0%, 80%);
|
|
--sidebar-primary: hsl(255, 70%, 62%);
|
|
--sidebar-primary-foreground: hsl(0, 0%, 98%);
|
|
--sidebar-accent: hsl(0, 0%, 18%);
|
|
--sidebar-accent-foreground: hsl(240, 4.8%, 95.9%);
|
|
--sidebar-border: hsl(0, 0%, 18%);
|
|
--sidebar-ring: hsl(255, 70%, 62%);
|
|
}
|
|
|
|
@layer base {
|
|
* {
|
|
@apply border-border;
|
|
}
|
|
|
|
body {
|
|
@apply bg-background text-foreground;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Uppy Dashboard: align with the app's design system. The theme
|
|
* (light/dark) is passed from React in sync with the app's toggle;
|
|
* these overrides handle typography, radius, and brand colors — our
|
|
* CSS variables flip with the theme automatically.
|
|
*/
|
|
.uppy-Dashboard-inner {
|
|
font-family: var(--font-sans);
|
|
border-radius: var(--radius-lg);
|
|
border-color: var(--border);
|
|
}
|
|
|
|
.uppy-Dashboard-AddFiles {
|
|
border-radius: calc(var(--radius-lg) - 4px);
|
|
}
|
|
|
|
.uppy-Dashboard-AddFiles-title {
|
|
font-size: 1rem;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.uppy-Dashboard-browse {
|
|
color: var(--primary);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.uppy-Dashboard-browse:hover,
|
|
.uppy-Dashboard-browse:focus {
|
|
border-bottom-color: var(--primary);
|
|
}
|
|
|
|
.uppy-DashboardContent-bar {
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.uppy-DashboardContent-back,
|
|
.uppy-DashboardContent-addMore {
|
|
color: var(--primary);
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
padding: 6px 10px;
|
|
border-radius: var(--radius-sm);
|
|
}
|
|
|
|
.uppy-DashboardContent-back:hover,
|
|
.uppy-DashboardContent-addMore:hover {
|
|
background-color: var(--accent);
|
|
color: var(--accent-foreground);
|
|
}
|
|
|
|
.uppy-DashboardContent-addMore svg {
|
|
width: 14px;
|
|
height: 14px;
|
|
}
|
|
|
|
.uppy-StatusBar-actionBtn--upload,
|
|
.uppy-c-btn-primary {
|
|
background-color: var(--primary);
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.uppy-StatusBar-actionBtn--upload:hover,
|
|
.uppy-c-btn-primary:hover {
|
|
background-color: color-mix(in srgb, var(--primary) 85%, black);
|
|
}
|
|
|
|
.uppy-Dashboard-note,
|
|
.uppy-StatusBar-statusPrimary,
|
|
.uppy-StatusBar-statusSecondary {
|
|
font-size: 0.8125rem;
|
|
}
|
|
|
|
[data-uppy-theme='dark'] .uppy-Dashboard-inner {
|
|
background-color: var(--card);
|
|
}
|
|
|
|
/*
|
|
* The API guide, rendered from docs/api-guide.md by ApiDocsController.
|
|
* No typography plugin is installed, and adding one for a single page
|
|
* would be a lot of CSS for one screen — these are the handful of element
|
|
* styles that markdown actually produces, scoped so they cannot leak into
|
|
* the rest of the app.
|
|
*/
|
|
.api-guide h1 {
|
|
font-size: 1.5rem;
|
|
font-weight: 600;
|
|
margin-block: 2rem 0.75rem;
|
|
}
|
|
|
|
.api-guide h2 {
|
|
font-size: 1.125rem;
|
|
font-weight: 600;
|
|
margin-block: 2rem 0.5rem;
|
|
padding-top: 1.5rem;
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
|
|
.api-guide h3 {
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
margin-block: 1.25rem 0.375rem;
|
|
}
|
|
|
|
.api-guide p,
|
|
.api-guide ul,
|
|
.api-guide ol {
|
|
margin-block: 0.75rem;
|
|
line-height: 1.65;
|
|
}
|
|
|
|
.api-guide ul,
|
|
.api-guide ol {
|
|
padding-inline-start: 1.5rem;
|
|
}
|
|
|
|
.api-guide ul {
|
|
list-style: disc;
|
|
}
|
|
|
|
.api-guide ol {
|
|
list-style: decimal;
|
|
}
|
|
|
|
.api-guide li {
|
|
margin-block: 0.25rem;
|
|
}
|
|
|
|
.api-guide a {
|
|
color: var(--primary);
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.api-guide code {
|
|
background-color: var(--muted);
|
|
border-radius: 0.25rem;
|
|
padding: 0.1rem 0.3rem;
|
|
font-size: 0.8125rem;
|
|
}
|
|
|
|
.api-guide pre {
|
|
background-color: var(--muted);
|
|
border-radius: 0.375rem;
|
|
padding: 0.875rem 1rem;
|
|
overflow-x: auto;
|
|
margin-block: 0.875rem;
|
|
}
|
|
|
|
.api-guide pre code {
|
|
background: none;
|
|
padding: 0;
|
|
font-size: 0.8125rem;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.api-guide table {
|
|
width: 100%;
|
|
margin-block: 1rem;
|
|
font-size: 0.875rem;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.api-guide th,
|
|
.api-guide td {
|
|
border: 1px solid var(--border);
|
|
padding: 0.5rem 0.75rem;
|
|
text-align: left;
|
|
vertical-align: top;
|
|
}
|
|
|
|
.api-guide th {
|
|
background-color: color-mix(in srgb, var(--muted) 60%, transparent);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.api-guide blockquote {
|
|
border-inline-start: 3px solid var(--border);
|
|
padding-inline-start: 1rem;
|
|
margin-block: 1rem;
|
|
color: var(--muted-foreground);
|
|
}
|
|
|
|
.api-guide hr {
|
|
border: 0;
|
|
border-top: 1px solid var(--border);
|
|
margin-block: 2rem;
|
|
}
|