Rebuild Noo site with interactive mockups and add privacy policy
Deploy noo (noo.ayushya.dev) / deploy (push) Successful in 15s
Deploy noo (noo.ayushya.dev) / deploy (push) Successful in 15s
Use the app's monochrome icon, add live phone mockups for accounts, locks, sync, sharing and appearance options, a tab list, coming-soon platforms, and a full privacy policy page. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
+14
-13
@@ -10,21 +10,23 @@ product.
|
|||||||
|
|
||||||
## Files
|
## Files
|
||||||
|
|
||||||
- `index.html` — the whole page (hero, features, customize, privacy, CTA).
|
- `index.html` — the landing page (hero, feature showcases, the
|
||||||
- `styles.css` — all styling, incl. the design tokens as CSS custom
|
Customize lab, tab list, privacy teaser, download CTA).
|
||||||
|
- `privacy.html` — the full privacy policy.
|
||||||
|
- `styles.css` — all styling, incl. the design tokens as CSS custom
|
||||||
properties and a dark-mode variant (`prefers-color-scheme` and an
|
properties and a dark-mode variant (`prefers-color-scheme` and an
|
||||||
explicit `data-theme` override for a future toggle).
|
explicit `data-theme` override).
|
||||||
- `script.js` — mobile nav, the hero's exploded-layer parallax
|
- `script.js` — renders the interactive phone mockups from the design
|
||||||
(scroll + pointer), the two interactive Customize demos (accent color,
|
tokens (bottom bar, nav menu, tabs, accents, AMOLED, action bar,
|
||||||
bottom bar style), and scroll-reveal.
|
swiping, share sheet, locks, sync, accounts), plus nav and reveal.
|
||||||
|
- `icons.svg` — SVG sprite of the Lucide icons the mockups use
|
||||||
|
(`<use href="icons.svg#i-name">`; needs an http server, not `file://`).
|
||||||
|
- `noo-mark.png` — the app's monochrome icon, tinted via CSS mask.
|
||||||
- `favicon.svg`.
|
- `favicon.svg`.
|
||||||
|
|
||||||
## Local preview
|
## Local preview
|
||||||
|
|
||||||
Any static file server works, e.g.:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd website
|
|
||||||
python3 -m http.server 8000
|
python3 -m http.server 8000
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -32,7 +34,6 @@ Then open `http://localhost:8000`.
|
|||||||
|
|
||||||
## Deploying
|
## Deploying
|
||||||
|
|
||||||
Point any static host (Cloudflare Pages, Netlify, GitHub Pages, or a
|
Deployed as Cloudflare Workers static assets (`wrangler.toml`); nothing
|
||||||
plain nginx `root`) at this folder - there's nothing to build. Update the
|
to build. Replace the `#` download links in `index.html` once real
|
||||||
`https://github.com` placeholder links and the two "Download for..."
|
release links exist.
|
||||||
buttons in `index.html` once real release links exist.
|
|
||||||
|
|||||||
+1
-4
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 214 B After Width: | Height: | Size: 49 KiB |
@@ -0,0 +1,81 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" style="display:none">
|
||||||
|
<symbol id="i-menu" viewBox="0 0 24 24"><line x1="4" x2="20" y1="12" y2="12" /><line x1="4" x2="20" y1="6" y2="6" /><line x1="4" x2="20" y1="18" y2="18" /></symbol>
|
||||||
|
<symbol id="i-search" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" /></symbol>
|
||||||
|
<symbol id="i-plus" viewBox="0 0 24 24"><path d="M5 12h14" /><path d="M12 5v14" /></symbol>
|
||||||
|
<symbol id="i-x" viewBox="0 0 24 24"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></symbol>
|
||||||
|
<symbol id="i-check" viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5" /></symbol>
|
||||||
|
<symbol id="i-chevR" viewBox="0 0 24 24"><path d="m9 18 6-6-6-6" /></symbol>
|
||||||
|
<symbol id="i-chevD" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6" /></symbol>
|
||||||
|
<symbol id="i-chevL" viewBox="0 0 24 24"><path d="m15 18-6-6 6-6" /></symbol>
|
||||||
|
<symbol id="i-back" viewBox="0 0 24 24"><path d="m12 19-7-7 7-7" /><path d="M19 12H5" /></symbol>
|
||||||
|
<symbol id="i-sort" viewBox="0 0 24 24"><path d="m21 16-4 4-4-4" /><path d="M17 20V4" /><path d="m3 8 4-4 4 4" /><path d="M7 4v16" /></symbol>
|
||||||
|
<symbol id="i-up" viewBox="0 0 24 24"><path d="m5 12 7-7 7 7" /><path d="M12 19V5" /></symbol>
|
||||||
|
<symbol id="i-filter" viewBox="0 0 24 24"><path d="M3 6h18" /><path d="M7 12h10" /><path d="M10 18h4" /></symbol>
|
||||||
|
<symbol id="i-list" viewBox="0 0 24 24"><path d="M3 12h.01" /><path d="M3 18h.01" /><path d="M3 6h.01" /><path d="M8 12h13" /><path d="M8 18h13" /><path d="M8 6h13" /></symbol>
|
||||||
|
<symbol id="i-grid" viewBox="0 0 24 24"><rect width="7" height="7" x="3" y="3" rx="1" /><rect width="7" height="7" x="14" y="3" rx="1" /><rect width="7" height="7" x="14" y="14" rx="1" /><rect width="7" height="7" x="3" y="14" rx="1" /></symbol>
|
||||||
|
<symbol id="i-folder" viewBox="0 0 24 24"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" /></symbol>
|
||||||
|
<symbol id="i-doc" viewBox="0 0 24 24"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" /><path d="M14 2v4a2 2 0 0 0 2 2h4" /><path d="M10 9H8" /><path d="M16 13H8" /><path d="M16 17H8" /></symbol>
|
||||||
|
<symbol id="i-sheet" viewBox="0 0 24 24"><rect width="18" height="18" x="3" y="3" rx="2" ry="2" /><line x1="3" x2="21" y1="9" y2="9" /><line x1="3" x2="21" y1="15" y2="15" /><line x1="9" x2="9" y1="9" y2="21" /><line x1="15" x2="15" y1="9" y2="21" /></symbol>
|
||||||
|
<symbol id="i-film" viewBox="0 0 24 24"><rect width="18" height="18" x="3" y="3" rx="2" /><path d="M7 3v18" /><path d="M3 7.5h4" /><path d="M3 12h18" /><path d="M3 16.5h4" /><path d="M17 3v18" /><path d="M17 7.5h4" /><path d="M17 16.5h4" /></symbol>
|
||||||
|
<symbol id="i-image" viewBox="0 0 24 24"><rect width="18" height="18" x="3" y="3" rx="2" ry="2" /><circle cx="9" cy="9" r="2" /><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" /></symbol>
|
||||||
|
<symbol id="i-zip" viewBox="0 0 24 24"><path d="M10 12v-1" /><path d="M10 18v-2" /><path d="M10 7V6" /><path d="M14 2v4a2 2 0 0 0 2 2h4" /><path d="M15.5 22H18a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v16a2 2 0 0 0 .274 1.01" /><circle cx="10" cy="20" r="2" /></symbol>
|
||||||
|
<symbol id="i-star" viewBox="0 0 24 24"><path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z" /></symbol>
|
||||||
|
<symbol id="i-moreV" viewBox="0 0 24 24"><circle cx="12" cy="12" r="1" /><circle cx="12" cy="5" r="1" /><circle cx="12" cy="19" r="1" /></symbol>
|
||||||
|
<symbol id="i-moreH" viewBox="0 0 24 24"><circle cx="12" cy="12" r="1" /><circle cx="19" cy="12" r="1" /><circle cx="5" cy="12" r="1" /></symbol>
|
||||||
|
<symbol id="i-ok" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" /><path d="m9 12 2 2 4-4" /></symbol>
|
||||||
|
<symbol id="i-sync" viewBox="0 0 24 24"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" /><path d="M21 3v5h-5" /><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" /><path d="M8 16H3v5" /></symbol>
|
||||||
|
<symbol id="i-alert" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" /><line x1="12" x2="12" y1="8" y2="12" /><line x1="12" x2="12.01" y1="16" y2="16" /></symbol>
|
||||||
|
<symbol id="i-photos" viewBox="0 0 24 24"><path d="M18 22H4a2 2 0 0 1-2-2V6" /><path d="m22 13-1.296-1.296a2.41 2.41 0 0 0-3.408 0L11 18" /><circle cx="12" cy="8" r="2" /><rect width="16" height="16" x="6" y="2" rx="2" /></symbol>
|
||||||
|
<symbol id="i-offline" viewBox="0 0 24 24"><path d="M12 2v8" /><path d="m16 6-4 4-4-4" /><rect width="20" height="8" x="2" y="14" rx="2" /><path d="M6 18h.01" /><path d="M10 18h.01" /></symbol>
|
||||||
|
<symbol id="i-share" viewBox="0 0 24 24"><circle cx="18" cy="5" r="3" /><circle cx="6" cy="12" r="3" /><circle cx="18" cy="19" r="3" /><line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /><line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /></symbol>
|
||||||
|
<symbol id="i-clock" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" /></symbol>
|
||||||
|
<symbol id="i-activity" viewBox="0 0 24 24"><path d="M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2" /></symbol>
|
||||||
|
<symbol id="i-trash" viewBox="0 0 24 24"><path d="M3 6h18" /><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" /><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" /><line x1="10" x2="10" y1="11" y2="17" /><line x1="14" x2="14" y1="11" y2="17" /></symbol>
|
||||||
|
<symbol id="i-settings" viewBox="0 0 24 24"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" /><circle cx="12" cy="12" r="3" /></symbol>
|
||||||
|
<symbol id="i-restore" viewBox="0 0 24 24"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" /><path d="M3 3v5h5" /></symbol>
|
||||||
|
<symbol id="i-link" viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></symbol>
|
||||||
|
<symbol id="i-copy" viewBox="0 0 24 24"><rect width="14" height="14" x="8" y="8" rx="2" ry="2" /><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" /></symbol>
|
||||||
|
<symbol id="i-userPlus" viewBox="0 0 24 24"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /><circle cx="9" cy="7" r="4" /><line x1="19" x2="19" y1="8" y2="14" /><line x1="22" x2="16" y1="11" y2="11" /></symbol>
|
||||||
|
<symbol id="i-users" viewBox="0 0 24 24"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /><circle cx="9" cy="7" r="4" /><path d="M22 21v-2a4 4 0 0 0-3-3.87" /><path d="M16 3.13a4 4 0 0 1 0 7.75" /></symbol>
|
||||||
|
<symbol id="i-lock" viewBox="0 0 24 24"><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /><path d="M7 11V7a5 5 0 0 1 10 0v4" /></symbol>
|
||||||
|
<symbol id="i-fingerprint" viewBox="0 0 24 24"><path d="M12 10a2 2 0 0 0-2 2c0 1.02-.1 2.51-.26 4" /><path d="M14 13.12c0 2.38 0 6.38-1 8.88" /><path d="M17.29 21.02c.12-.6.43-2.3.5-3.02" /><path d="M2 12a10 10 0 0 1 18-6" /><path d="M2 16h.01" /><path d="M21.8 16c.2-2 .131-5.354 0-6" /><path d="M5 19.5C5.5 18 6 15 6 12a6 6 0 0 1 .34-2" /><path d="M8.65 22c.21-.66.45-1.32.57-2" /><path d="M9 6.8a6 6 0 0 1 9 5.2v2" /></symbol>
|
||||||
|
<symbol id="i-face" viewBox="0 0 24 24"><path d="M3 7V5a2 2 0 0 1 2-2h2" /><path d="M17 3h2a2 2 0 0 1 2 2v2" /><path d="M21 17v2a2 2 0 0 1-2 2h-2" /><path d="M7 21H5a2 2 0 0 1-2-2v-2" /><path d="M8 14s1.5 2 4 2 4-2 4-2" /><path d="M9 9h.01" /><path d="M15 9h.01" /></symbol>
|
||||||
|
<symbol id="i-calendar" viewBox="0 0 24 24"><path d="M8 2v4" /><path d="M16 2v4" /><rect width="18" height="18" x="3" y="4" rx="2" /><path d="M3 10h18" /></symbol>
|
||||||
|
<symbol id="i-send" viewBox="0 0 24 24"><path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z" /><path d="m21.854 2.147-10.94 10.939" /></symbol>
|
||||||
|
<symbol id="i-grip" viewBox="0 0 24 24"><circle cx="9" cy="12" r="1" /><circle cx="9" cy="5" r="1" /><circle cx="9" cy="19" r="1" /><circle cx="15" cy="12" r="1" /><circle cx="15" cy="5" r="1" /><circle cx="15" cy="19" r="1" /></symbol>
|
||||||
|
<symbol id="i-pin" viewBox="0 0 24 24"><path d="M12 17v5" /><path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z" /></symbol>
|
||||||
|
<symbol id="i-pinOff" viewBox="0 0 24 24"><path d="M12 17v5" /><path d="M15 9.34V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H7.89" /><path d="m2 2 20 20" /><path d="M9 9v1.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h11" /></symbol>
|
||||||
|
<symbol id="i-info" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" /><path d="M12 16v-4" /><path d="M12 8h.01" /></symbol>
|
||||||
|
<symbol id="i-logout" viewBox="0 0 24 24"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" /><polyline points="16 17 21 12 16 7" /><line x1="21" x2="9" y1="12" y2="12" /></symbol>
|
||||||
|
<symbol id="i-server" viewBox="0 0 24 24"><rect width="20" height="8" x="2" y="2" rx="2" ry="2" /><rect width="20" height="8" x="2" y="14" rx="2" ry="2" /><line x1="6" x2="6.01" y1="6" y2="6" /><line x1="6" x2="6.01" y1="18" y2="18" /></symbol>
|
||||||
|
<symbol id="i-wifi" viewBox="0 0 24 24"><path d="M12 20h.01" /><path d="M2 8.82a15 15 0 0 1 20 0" /><path d="M5 12.859a10 10 0 0 1 14 0" /><path d="M8.5 16.429a5 5 0 0 1 7 0" /></symbol>
|
||||||
|
<symbol id="i-signal" viewBox="0 0 24 24"><path d="M2 20h.01" /><path d="M7 20v-4" /><path d="M12 20v-8" /><path d="M17 20V8" /><path d="M22 4v16" /></symbol>
|
||||||
|
<symbol id="i-battery" viewBox="0 0 24 24"><rect width="16" height="10" x="2" y="7" rx="2" ry="2" /><line x1="22" x2="22" y1="11" y2="13" /><line x1="6" x2="6" y1="11" y2="13" /><line x1="10" x2="10" y1="11" y2="13" /><line x1="14" x2="14" y1="11" y2="13" /></symbol>
|
||||||
|
<symbol id="i-charging" viewBox="0 0 24 24"><path d="M15 7h1a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2h-2" /><path d="M6 7H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h1" /><path d="m11 7-3 5h4l-3 5" /><line x1="22" x2="22" y1="11" y2="13" /></symbol>
|
||||||
|
<symbol id="i-camera" viewBox="0 0 24 24"><path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z" /><circle cx="12" cy="13" r="3" /></symbol>
|
||||||
|
<symbol id="i-database" viewBox="0 0 24 24"><ellipse cx="12" cy="5" rx="9" ry="3" /><path d="M3 5V19A9 3 0 0 0 21 19V5" /><path d="M3 12A9 3 0 0 0 21 12" /></symbol>
|
||||||
|
<symbol id="i-palette" viewBox="0 0 24 24"><circle cx="13.5" cy="6.5" r=".5" fill="currentColor" /><circle cx="17.5" cy="10.5" r=".5" fill="currentColor" /><circle cx="8.5" cy="7.5" r=".5" fill="currentColor" /><circle cx="6.5" cy="12.5" r=".5" fill="currentColor" /><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z" /></symbol>
|
||||||
|
<symbol id="i-swipe" viewBox="0 0 24 24"><path d="m18 8 4 4-4 4" /><path d="M2 12h20" /><path d="m6 8-4 4 4 4" /></symbol>
|
||||||
|
<symbol id="i-upload" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline points="17 8 12 3 7 8" /><line x1="12" x2="12" y1="3" y2="15" /></symbol>
|
||||||
|
<symbol id="i-play" viewBox="0 0 24 24"><polygon points="6 3 20 12 6 21 6 3" /></symbol>
|
||||||
|
<symbol id="i-pencil" viewBox="0 0 24 24"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" /><path d="m15 5 4 4" /></symbol>
|
||||||
|
<symbol id="i-timer" viewBox="0 0 24 24"><line x1="10" x2="14" y1="2" y2="2" /><line x1="12" x2="15" y1="14" y2="11" /><circle cx="12" cy="14" r="8" /></symbol>
|
||||||
|
<symbol id="i-minus" viewBox="0 0 24 24"><path d="M5 12h14" /></symbol>
|
||||||
|
<symbol id="i-square" viewBox="0 0 24 24"><rect width="18" height="18" x="3" y="3" rx="2" /></symbol>
|
||||||
|
<symbol id="i-panel" viewBox="0 0 24 24"><rect width="18" height="18" x="3" y="3" rx="2" /><path d="M9 3v18" /></symbol>
|
||||||
|
<symbol id="i-download" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline points="7 10 12 15 17 10" /><line x1="12" x2="12" y1="15" y2="3" /></symbol>
|
||||||
|
<symbol id="i-hidden" viewBox="0 0 24 24"><path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49" /><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242" /><path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143" /><path d="m2 2 20 20" /></symbol>
|
||||||
|
<symbol id="i-eye" viewBox="0 0 24 24"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0" /><circle cx="12" cy="12" r="3" /></symbol>
|
||||||
|
<symbol id="i-external" viewBox="0 0 24 24"><path d="M15 3h6v6" /><path d="M10 14 21 3" /><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></symbol>
|
||||||
|
<symbol id="i-key" viewBox="0 0 24 24"><path d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z" /><circle cx="16.5" cy="7.5" r=".5" fill="currentColor" /></symbol>
|
||||||
|
<symbol id="i-monitor" viewBox="0 0 24 24"><rect width="20" height="14" x="2" y="3" rx="2" /><line x1="8" x2="16" y1="21" y2="21" /><line x1="12" x2="12" y1="17" y2="21" /></symbol>
|
||||||
|
<symbol id="i-plusCircle" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" /><path d="M8 12h8" /><path d="M12 8v8" /></symbol>
|
||||||
|
<symbol id="i-move" viewBox="0 0 24 24"><path d="M2 9V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-1"/><path d="M2 13h10"/><path d="m9 16 3-3-3-3"/></symbol>
|
||||||
|
<symbol id="i-rename" viewBox="0 0 24 24"><path d="M12.5 22H18a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v9.5"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M13.378 15.626a1 1 0 1 0-3.004-3.004l-5.01 5.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"/></symbol>
|
||||||
|
<symbol id="i-switch" viewBox="0 0 24 24"><path d="M8 3 4 7l4 4"/><path d="M4 7h16"/><path d="m16 21 4-4-4-4"/><path d="M20 17H4"/></symbol>
|
||||||
|
<symbol id="i-cloud" viewBox="0 0 24 24"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z"/></symbol>
|
||||||
|
<symbol id="i-refresh" viewBox="0 0 24 24"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/></symbol>
|
||||||
|
<symbol id="i-wallpaper" viewBox="0 0 24 24"><circle cx="8" cy="9" r="2"/><path d="M20 16 15.29 11.3a2 2 0 0 0-2.83 0L5 19"/><rect x="3" y="3" width="18" height="18" rx="2"/></symbol>
|
||||||
|
<symbol id="i-bell" viewBox="0 0 24 24"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></symbol>
|
||||||
|
<symbol id="i-mail" viewBox="0 0 24 24"><rect width="20" height="16" x="2" y="4" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></symbol>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 15 KiB |
+372
-307
@@ -1,307 +1,372 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Noo — a Nextcloud client that feels like it belongs on your phone</title>
|
<title>Noo — a Nextcloud client that feels like it belongs on your phone</title>
|
||||||
<meta name="description" content="Noo is a fast, private, deeply customizable Nextcloud client for Android and iOS. Browse files and photos, sync for offline, and make it look like yours.">
|
<meta name="description" content="Noo is a fast, private, deeply customizable Nextcloud client. Multiple accounts, biometric locks, offline and background sync, and a shell you can rebuild around your thumb.">
|
||||||
<meta property="og:title" content="Noo — a Nextcloud client that feels like it belongs on your phone">
|
<meta property="og:title" content="Noo — a Nextcloud client that feels like it belongs on your phone">
|
||||||
<meta property="og:description" content="Fast, private, and deeply customizable. Browse files and photos, sync for offline, and make Noo look like yours.">
|
<meta property="og:description" content="Multiple accounts, locks, offline sync, native-feeling sharing, and an interface you can reshape. No analytics, no third parties.">
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<meta property="og:url" content="https://noo.ayushya.dev">
|
<meta property="og:url" content="https://noo.ayushya.dev">
|
||||||
<meta name="theme-color" content="#F3EFE6">
|
<meta name="theme-color" content="#F3EFE6">
|
||||||
<link rel="icon" href="favicon.svg" type="image/svg+xml">
|
<link rel="icon" href="favicon.svg" type="image/svg+xml">
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600&family=Schibsted+Grotesk:wght@500;600;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600;700&family=Schibsted+Grotesk:wght@500;600;700&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="styles.css">
|
<link rel="stylesheet" href="styles.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<a class="skip-link" href="#main">Skip to content</a>
|
<a class="skip-link" href="#main">Skip to content</a>
|
||||||
|
|
||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
<div class="wrap header-row">
|
<div class="wrap header-row">
|
||||||
<a class="wordmark" href="#top">
|
<a class="wordmark" href="#top">
|
||||||
<span class="wordmark-mark" aria-hidden="true"></span>
|
<span class="wordmark-mark" aria-hidden="true"></span>
|
||||||
noo
|
noo
|
||||||
</a>
|
</a>
|
||||||
<nav class="site-nav" aria-label="Primary">
|
<nav class="site-nav" aria-label="Primary">
|
||||||
<a href="#features">Features</a>
|
<a href="#features">Features</a>
|
||||||
<a href="#customize">Customize</a>
|
<a href="#customize">Customize</a>
|
||||||
<a href="#privacy">Privacy</a>
|
<a href="#tabs">Tabs</a>
|
||||||
</nav>
|
<a href="privacy.html">Privacy</a>
|
||||||
<a class="btn btn-primary btn-sm" href="#get">Get Noo</a>
|
</nav>
|
||||||
<button class="nav-toggle" id="navToggle" aria-label="Open menu" aria-expanded="false">
|
<a class="btn btn-primary btn-sm" href="#get">Get Noo</a>
|
||||||
<span></span><span></span><span></span>
|
<button class="nav-toggle" id="navToggle" aria-label="Open menu" aria-expanded="false">
|
||||||
</button>
|
<span></span><span></span><span></span>
|
||||||
</div>
|
</button>
|
||||||
<nav class="mobile-nav" id="mobileNav" aria-label="Primary">
|
</div>
|
||||||
<a href="#features">Features</a>
|
<nav class="mobile-nav" id="mobileNav" aria-label="Primary">
|
||||||
<a href="#customize">Customize</a>
|
<a href="#features">Features</a>
|
||||||
<a href="#privacy">Privacy</a>
|
<a href="#customize">Customize</a>
|
||||||
<a class="btn btn-primary btn-sm" href="#get">Get Noo</a>
|
<a href="#tabs">Tabs</a>
|
||||||
</nav>
|
<a href="privacy.html">Privacy</a>
|
||||||
</header>
|
<a class="btn btn-primary btn-sm" href="#get">Get Noo</a>
|
||||||
|
</nav>
|
||||||
<main id="main">
|
</header>
|
||||||
|
|
||||||
<!-- ============ HERO ============ -->
|
<main id="main">
|
||||||
<section class="hero" id="top">
|
|
||||||
<div class="wrap hero-grid">
|
<!-- ============ HERO ============ -->
|
||||||
<div class="hero-copy">
|
<section class="hero" id="top">
|
||||||
<p class="eyebrow">For Nextcloud, unofficial</p>
|
<div class="wrap hero-grid">
|
||||||
<h1>Your cloud.<br>Looking like <em>yours</em>.</h1>
|
<div class="hero-copy">
|
||||||
<p class="hero-sub">
|
<p class="eyebrow">For Nextcloud, unofficial</p>
|
||||||
Noo is a fast, private Nextcloud client that browses files and
|
<h1>Your cloud.<br>Looking like <em>yours</em>.</h1>
|
||||||
photos the way a native app should — and lets you rebuild its
|
<p class="hero-sub">
|
||||||
whole shell around how you actually use your phone.
|
Noo is a fast, private Nextcloud client that browses files and
|
||||||
</p>
|
photos the way a native app should — and lets you rebuild its
|
||||||
<div class="hero-actions">
|
whole shell around how you actually use your phone.
|
||||||
<a class="btn btn-primary btn-lg" href="#get">Get Noo</a>
|
</p>
|
||||||
<a class="btn btn-outline btn-lg" href="https://github.com" target="_blank" rel="noopener">View source</a>
|
<div class="hero-actions">
|
||||||
</div>
|
<a class="btn btn-primary btn-lg" href="#get">Get Noo</a>
|
||||||
<ul class="hero-proof">
|
<a class="btn btn-outline btn-lg" href="#customize">Try the customizer</a>
|
||||||
<li>No password stored — Login Flow v2</li>
|
</div>
|
||||||
<li>Offline device sync</li>
|
<ul class="hero-proof">
|
||||||
<li>Open source</li>
|
<li>No password stored — Login Flow v2</li>
|
||||||
</ul>
|
<li>Talks only to your server</li>
|
||||||
</div>
|
<li>No analytics, no trackers</li>
|
||||||
|
</ul>
|
||||||
<div class="hero-visual" id="heroVisual" aria-hidden="true">
|
</div>
|
||||||
<div class="glow"></div>
|
|
||||||
|
<div class="hero-visual" id="heroVisual">
|
||||||
<div class="phone" data-depth="0.15">
|
<div class="glow"></div>
|
||||||
<div class="phone-notch"></div>
|
<div class="phone-wrap" data-depth="0.15"><div id="heroPhone" class="phone-host"></div></div>
|
||||||
<div class="phone-screen">
|
|
||||||
<div class="mini-topbar">
|
<div class="chip chip-photo" data-depth="0.55" aria-hidden="true">
|
||||||
<span class="mini-title">Files</span>
|
<div class="chip-photo-grid"><span></span><span></span><span></span><span></span></div>
|
||||||
<span class="mini-dot"></span>
|
</div>
|
||||||
</div>
|
<div class="chip chip-share" data-depth="0.4" aria-hidden="true">
|
||||||
<div class="mini-list">
|
<svg class="i" width="18" height="18"><use href="icons.svg#i-share"/></svg>
|
||||||
<div class="mini-row"><span class="mini-ic ic-folder"></span><span class="mini-lines"><b></b><i></i></span></div>
|
<span><b>Ana Souza</b><i>Can edit</i></span>
|
||||||
<div class="mini-row"><span class="mini-ic ic-image"></span><span class="mini-lines"><b></b><i></i></span></div>
|
</div>
|
||||||
<div class="mini-row mini-row-active"><span class="mini-ic ic-file"></span><span class="mini-lines"><b></b><i></i></span></div>
|
<div class="chip chip-lock" data-depth="0.7" aria-hidden="true">
|
||||||
<div class="mini-row"><span class="mini-ic ic-folder"></span><span class="mini-lines"><b></b><i></i></span></div>
|
<svg class="i" width="18" height="18"><use href="icons.svg#i-fingerprint"/></svg>
|
||||||
</div>
|
<span>Locked</span>
|
||||||
<div class="mini-bar">
|
</div>
|
||||||
<span class="mini-bar-pill"></span>
|
<div class="chip chip-badge" data-depth="0.85" aria-hidden="true">
|
||||||
<span class="mini-bar-dot"></span>
|
<svg class="i" width="16" height="16"><use href="icons.svg#i-offline"/></svg>
|
||||||
<span class="mini-bar-dot"></span>
|
Offline ready
|
||||||
<span class="mini-bar-dot"></span>
|
</div>
|
||||||
<span class="mini-bar-dot"></span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
|
||||||
|
<!-- ============ FEATURES ============ -->
|
||||||
<div class="chip chip-photo" data-depth="0.55">
|
<section class="section" id="features">
|
||||||
<div class="chip-photo-grid">
|
<div class="wrap">
|
||||||
<span></span><span></span><span></span><span></span>
|
<p class="eyebrow">Features</p>
|
||||||
</div>
|
<h2>Everything your Nextcloud does. Nothing it doesn't.</h2>
|
||||||
</div>
|
<p class="section-sub">
|
||||||
|
Built as a real mobile app, not a browser tab pinned to your home
|
||||||
<div class="chip chip-row" data-depth="0.4">
|
screen. Every demo below is live — poke at it.
|
||||||
<span class="mini-ic ic-share"></span>
|
</p>
|
||||||
<span class="mini-lines"><b></b><i></i></span>
|
|
||||||
</div>
|
<!-- Accounts -->
|
||||||
|
<div class="showcase">
|
||||||
<div class="chip chip-swatches" data-depth="0.7">
|
<div class="showcase-copy">
|
||||||
<span class="swatch" style="--c:#8D0DE3"></span>
|
<span class="feat-ic"><svg class="i" width="22" height="22"><use href="icons.svg#i-users"/></svg></span>
|
||||||
<span class="swatch" style="--c:#2940A8"></span>
|
<h3>Multiple accounts, one tap apart</h3>
|
||||||
<span class="swatch" style="--c:#3E7A23"></span>
|
<p>Keep your personal server, your work instance and the family
|
||||||
<span class="swatch" style="--c:#8C5A0E"></span>
|
box signed in at once. Switch from the avatar or the Settings
|
||||||
</div>
|
list — each account keeps its own sync scope, cache and
|
||||||
|
favorites, and you can lock switching behind your fingerprint.</p>
|
||||||
<div class="chip chip-toggle" data-depth="0.5">
|
<p class="demo-note" id="acctNote">Tap an account to make it active.</p>
|
||||||
<span class="mini-lines mini-lines-toggle"><b></b><i></i></span>
|
</div>
|
||||||
<span class="mini-toggle mini-toggle-on"><i></i></span>
|
<div class="showcase-demo">
|
||||||
</div>
|
<div class="mock-card">
|
||||||
|
<h5 class="mock-title">Accounts</h5>
|
||||||
<div class="chip chip-badge" data-depth="0.85">
|
<div class="mock-group" id="acctDemo"></div>
|
||||||
<span class="mini-ic ic-lock"></span>
|
</div>
|
||||||
Offline ready
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
<!-- Locks -->
|
||||||
</section>
|
<div class="showcase showcase-flip">
|
||||||
|
<div class="showcase-copy">
|
||||||
<!-- ============ FEATURES ============ -->
|
<span class="feat-ic"><svg class="i" width="22" height="22"><use href="icons.svg#i-lock"/></svg></span>
|
||||||
<section class="section" id="features">
|
<h3>Locks where you want them</h3>
|
||||||
<div class="wrap">
|
<p>Gate opening the app, switching accounts, and revealing hidden
|
||||||
<p class="eyebrow">Features</p>
|
files behind your device's own PIN, pattern or biometric. Noo
|
||||||
<h2>Everything your Nextcloud does. Nothing it doesn't.</h2>
|
never stores or even sees a PIN — it just asks the OS whether you
|
||||||
<p class="section-sub">
|
are you.</p>
|
||||||
Files, photos, sharing and sync — built as a real mobile app, not a
|
<p class="demo-note">The two extra locks need the main one on, just like in the app.</p>
|
||||||
browser tab pinned to your home screen.
|
</div>
|
||||||
</p>
|
<div class="showcase-demo">
|
||||||
|
<div class="mock-card">
|
||||||
<div class="grid grid-features">
|
<h5 class="mock-title">Security</h5>
|
||||||
<article class="card">
|
<div class="mock-group" id="lockDemo"></div>
|
||||||
<span class="card-ic ic-folder-lg"></span>
|
</div>
|
||||||
<h3>Files, browsed properly</h3>
|
</div>
|
||||||
<p>Grid or list, sorted your way, with the same filters and
|
</div>
|
||||||
controls whether you're picking a file, uploading, or moving
|
|
||||||
one into place.</p>
|
<!-- Sync -->
|
||||||
</article>
|
<div class="showcase">
|
||||||
<article class="card">
|
<div class="showcase-copy">
|
||||||
<span class="card-ic ic-image-lg"></span>
|
<span class="feat-ic"><svg class="i" width="22" height="22"><use href="icons.svg#i-offline"/></svg></span>
|
||||||
<h3>Photos, grouped by month</h3>
|
<h3>Offline and background sync</h3>
|
||||||
<p>Every image and video across your account in one gallery —
|
<p>Mirror a few folders — or the whole account — to the device and
|
||||||
filter to just photos, just videos, or just favorites.</p>
|
keep reading, opening and previewing with no connection. Background
|
||||||
</article>
|
sync tops them up while you aren't looking, on Wi-Fi only unless
|
||||||
<article class="card">
|
you say otherwise, and it tells you when something conflicts.</p>
|
||||||
<span class="card-ic ic-download-lg"></span>
|
<p class="demo-note">Flip the options, then hit Sync now.</p>
|
||||||
<h3>Offline, for real</h3>
|
</div>
|
||||||
<p>Pick folders to mirror locally and keep browsing — reading,
|
<div class="showcase-demo">
|
||||||
opening, previewing — with no connection at all.</p>
|
<div class="mock-card">
|
||||||
</article>
|
<h5 class="mock-title">File sync</h5>
|
||||||
<article class="card">
|
<div class="mock-group" id="syncDemo"></div>
|
||||||
<span class="card-ic ic-share-lg"></span>
|
</div>
|
||||||
<h3>Sharing, without the detour</h3>
|
</div>
|
||||||
<p>Share with people or a public link, see who has access, and
|
</div>
|
||||||
copy that link — all from one sheet, not three menus.</p>
|
|
||||||
</article>
|
<!-- Sharing -->
|
||||||
<article class="card">
|
<div class="showcase showcase-flip">
|
||||||
<span class="card-ic ic-clock-lg"></span>
|
<div class="showcase-copy">
|
||||||
<h3>Activity & recent</h3>
|
<span class="feat-ic"><svg class="i" width="22" height="22"><use href="icons.svg#i-share"/></svg></span>
|
||||||
<p>What changed, and where — grouped into today, yesterday and
|
<h3>Sharing that feels like Nextcloud</h3>
|
||||||
this week, so catching up takes seconds.</p>
|
<p>The same share sheet you know from the web and the official
|
||||||
</article>
|
app: invite people or groups with a permission pill, switch a public
|
||||||
<article class="card">
|
link on and copy it, or skip all that and send the file's bytes
|
||||||
<span class="card-ic ic-users-lg"></span>
|
straight to another app. One sheet, not three menus.</p>
|
||||||
<h3>Multiple accounts</h3>
|
<p class="demo-note">Change a permission, remove someone, toggle the link.</p>
|
||||||
<p>Switch between personal and work Nextcloud instances with a
|
</div>
|
||||||
swipe — each with its own sync, cache and lock.</p>
|
<div class="showcase-demo">
|
||||||
</article>
|
<div class="sheet" id="shareDemo"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
</section>
|
||||||
<!-- ============ CUSTOMIZE ============ -->
|
|
||||||
<section class="section section-alt" id="customize">
|
<!-- ============ CUSTOMIZE ============ -->
|
||||||
<div class="wrap">
|
<section class="section section-alt" id="customize">
|
||||||
<p class="eyebrow">Customize</p>
|
<div class="wrap">
|
||||||
<h2>Shape it around your thumb, not the other way round.</h2>
|
<p class="eyebrow">Customize</p>
|
||||||
<p class="section-sub">
|
<h2>Shape it around your thumb, not the other way round.</h2>
|
||||||
Every screen bends to a handful of real settings — try a few
|
<p class="section-sub">
|
||||||
right here.
|
This is the real Noo layout, drawn from the app's own design
|
||||||
</p>
|
tokens. Change a setting on the right and watch the phone.
|
||||||
|
</p>
|
||||||
<div class="customize-grid">
|
|
||||||
|
<div class="lab">
|
||||||
<div class="customize-demo" id="accentDemo">
|
<div class="lab-phone">
|
||||||
<div class="demo-preview">
|
<div class="phone-wrap phone-wrap-lg"><div id="labPhone" class="phone-host"></div></div>
|
||||||
<div class="demo-topbar">
|
<p class="lab-hint">Tap the tabs, the menu and the avatar — they work.</p>
|
||||||
<span class="demo-dot" style="background:var(--demo-accent)"></span>
|
</div>
|
||||||
<span class="demo-line" style="width:38%"></span>
|
|
||||||
</div>
|
<div class="lab-controls">
|
||||||
<div class="demo-pill" style="background:var(--surface3); background:color-mix(in srgb, var(--demo-accent) 16%, transparent); color:var(--demo-accent)">Active tab</div>
|
|
||||||
<div class="demo-row"><span class="demo-line" style="width:60%"></span></div>
|
<section class="ctl">
|
||||||
<div class="demo-row"><span class="demo-line" style="width:44%"></span></div>
|
<h3>Themes</h3>
|
||||||
</div>
|
<p>Follow the system, or pick light or dark.</p>
|
||||||
<div class="demo-controls">
|
<div class="segmented segmented-full" role="group" aria-label="Theme" data-lab="theme">
|
||||||
<h3>Pick your accent</h3>
|
<button class="segmented-btn" data-val="system">System</button>
|
||||||
<p>Six built-in colors, or match your wallpaper with dynamic
|
<button class="segmented-btn" data-val="light">Light</button>
|
||||||
color on Android.</p>
|
<button class="segmented-btn" data-val="dark">Dark</button>
|
||||||
<div class="swatch-row" role="group" aria-label="Accent color">
|
</div>
|
||||||
<button class="swatch-btn is-active" style="--c:#8D0DE3" data-accent="#8D0DE3" aria-label="Violet"></button>
|
</section>
|
||||||
<button class="swatch-btn" style="--c:#2940A8" data-accent="#2940A8" aria-label="Blue"></button>
|
|
||||||
<button class="swatch-btn" style="--c:#3E7A23" data-accent="#3E7A23" aria-label="Green"></button>
|
<section class="ctl">
|
||||||
<button class="swatch-btn" style="--c:#8C5A0E" data-accent="#8C5A0E" aria-label="Amber"></button>
|
<h3>Colors</h3>
|
||||||
<button class="swatch-btn" style="--c:#A8202A" data-accent="#A8202A" aria-label="Red"></button>
|
<p>Six built-in accents, or let Android's wallpaper colors take over. <span id="labAccentName" class="ctl-value"></span></p>
|
||||||
<button class="swatch-btn" style="--c:#0E0D0B" data-accent="#0E0D0B" aria-label="Wallpaper"></button>
|
<div class="swatch-row" id="labAccents" role="group" aria-label="Accent color"></div>
|
||||||
</div>
|
<div class="ctl-row" id="amoledRow">
|
||||||
</div>
|
<span><b>AMOLED black</b><i>Pure black backgrounds in dark mode</i></span>
|
||||||
</div>
|
<button class="toggle" data-toggle="amoled" aria-label="AMOLED black"><i></i></button>
|
||||||
|
</div>
|
||||||
<div class="customize-demo" id="barDemo">
|
</section>
|
||||||
<div class="demo-preview demo-preview-bar">
|
|
||||||
<div class="demo-screen-fill"></div>
|
<section class="ctl">
|
||||||
<div class="demo-bar" id="demoBar">
|
<h3>Bottom bar</h3>
|
||||||
<span class="demo-bar-item is-on"></span>
|
<p>Flush with the edge, or floating free. Optionally park Search in the bar too.</p>
|
||||||
<span class="demo-bar-item"></span>
|
<div class="segmented segmented-full" role="group" aria-label="Bottom bar style" data-lab="bar">
|
||||||
<span class="demo-bar-item"></span>
|
<button class="segmented-btn" data-val="attached">Attached</button>
|
||||||
<span class="demo-bar-item"></span>
|
<button class="segmented-btn" data-val="floating">Floating</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="ctl-row">
|
||||||
<div class="demo-controls">
|
<span><b>Search in bottom bar</b><i>Takes one tab slot and clears the top bar</i></span>
|
||||||
<h3>Floating or attached</h3>
|
<button class="toggle" data-toggle="searchInBar" aria-label="Search in bottom bar"><i></i></button>
|
||||||
<p>The bottom bar can sit flush with the edge, or float free
|
</div>
|
||||||
with rounded corners — your call.</p>
|
</section>
|
||||||
<div class="segmented" role="group" aria-label="Bottom bar style">
|
|
||||||
<button class="segmented-btn" data-bar="attached">Attached</button>
|
<section class="ctl">
|
||||||
<button class="segmented-btn is-active" data-bar="floating">Floating</button>
|
<h3>Navigation menu</h3>
|
||||||
</div>
|
<p>A hamburger that opens a drawer, or fold everything into the avatar.</p>
|
||||||
</div>
|
<div class="segmented segmented-full" role="group" aria-label="Navigation menu" data-lab="nav">
|
||||||
</div>
|
<button class="segmented-btn" data-val="drawer">Hamburger</button>
|
||||||
|
<button class="segmented-btn" data-val="avatar">Avatar</button>
|
||||||
<div class="customize-list">
|
</div>
|
||||||
<h3>And the rest</h3>
|
</section>
|
||||||
<ul class="feature-list">
|
|
||||||
<li>Reorder tabs, hide the ones you never open</li>
|
<section class="ctl">
|
||||||
<li>Move Search into the bottom bar</li>
|
<h3>Tabs</h3>
|
||||||
<li>Light, dark, or match the system</li>
|
<p id="labTabsNote"></p>
|
||||||
<li>Pure black backgrounds in dark mode</li>
|
<ul class="tab-list" id="labTabs"></ul>
|
||||||
<li>Choose what swiping a file left or right does</li>
|
</section>
|
||||||
<li>Reorder the bulk-action bar for multi-select</li>
|
</div>
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div class="duo">
|
||||||
</div>
|
<div class="mock-card" id="actionDemo">
|
||||||
</div>
|
<h3>Action bar</h3>
|
||||||
</section>
|
<p>Decide which bulk actions live in the multi-select bar. The first three sit inline; the rest wait behind “More”. Drag to reorder and watch the bar.</p>
|
||||||
|
<div class="selbar-stage" id="actionBar"></div>
|
||||||
<!-- ============ PRIVACY ============ -->
|
<ul class="tab-list" id="actionOrder"></ul>
|
||||||
<section class="section" id="privacy">
|
</div>
|
||||||
<div class="wrap privacy-grid">
|
|
||||||
<div>
|
<div class="mock-card" id="swipeDemo">
|
||||||
<p class="eyebrow">Privacy</p>
|
<h3>Swiping</h3>
|
||||||
<h2>It's your server. Noo just visits.</h2>
|
<p>Pick what swiping a file does. Drag a row — release past halfway to peek, or all the way through to trigger it.</p>
|
||||||
<p class="section-sub">
|
<div class="swipe-pick">
|
||||||
Noo never asks for your password. Signing in hands off to your
|
<span>Swipe right</span>
|
||||||
Nextcloud server's own login page (Login Flow v2), which returns
|
<div class="segmented segmented-sm" role="group" aria-label="Swipe right" data-swipe="right">
|
||||||
a revocable app token — not your credentials. Nothing but that
|
<button class="segmented-btn" data-val="none">None</button>
|
||||||
token, and whatever you choose to sync, ever leaves your device.
|
<button class="segmented-btn" data-val="favorite">Favorite</button>
|
||||||
</p>
|
<button class="segmented-btn" data-val="delete">Delete</button>
|
||||||
</div>
|
<button class="segmented-btn" data-val="share">Share</button>
|
||||||
<ul class="privacy-points">
|
</div>
|
||||||
<li><span class="pt-ic ic-lock"></span> No password stored on device</li>
|
</div>
|
||||||
<li><span class="pt-ic ic-server"></span> Talks only to your own server</li>
|
<div class="swipe-pick">
|
||||||
<li><span class="pt-ic ic-code"></span> Open source, auditable</li>
|
<span>Swipe left</span>
|
||||||
<li><span class="pt-ic ic-off"></span> No analytics, no trackers</li>
|
<div class="segmented segmented-sm" role="group" aria-label="Swipe left" data-swipe="left">
|
||||||
</ul>
|
<button class="segmented-btn" data-val="none">None</button>
|
||||||
</div>
|
<button class="segmented-btn" data-val="favorite">Favorite</button>
|
||||||
</section>
|
<button class="segmented-btn" data-val="delete">Delete</button>
|
||||||
|
<button class="segmented-btn" data-val="share">Share</button>
|
||||||
<!-- ============ GET ============ -->
|
</div>
|
||||||
<section class="section section-cta" id="get">
|
</div>
|
||||||
<div class="wrap cta-inner">
|
<div class="swipe-stage">
|
||||||
<h2>Bring your Nextcloud with you.</h2>
|
<div class="swipe-list" id="swipeList"></div>
|
||||||
<p class="section-sub">Free and open source. Point it at your server and go.</p>
|
<div class="swipe-toast" id="swipeToast" role="status" aria-live="polite"></div>
|
||||||
<div class="hero-actions cta-actions">
|
</div>
|
||||||
<a class="btn btn-primary btn-lg" href="#">Download for Android</a>
|
</div>
|
||||||
<a class="btn btn-outline btn-lg" href="#">Download for iOS</a>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="cta-fine">Or <a href="https://github.com" target="_blank" rel="noopener">build it from source</a> — it's just Flutter.</p>
|
</section>
|
||||||
</div>
|
|
||||||
</section>
|
<!-- ============ TABS ============ -->
|
||||||
|
<section class="section" id="tabs">
|
||||||
</main>
|
<div class="wrap">
|
||||||
|
<p class="eyebrow">Tabs</p>
|
||||||
<footer class="site-footer">
|
<h2>Eight tabs. Pin the five you use.</h2>
|
||||||
<div class="wrap footer-row">
|
<p class="section-sub">
|
||||||
<a class="wordmark wordmark-sm" href="#top">
|
Every view below is a tab you can pin to the bottom bar, reorder,
|
||||||
<span class="wordmark-mark" aria-hidden="true"></span>
|
or leave tucked in the menu.
|
||||||
noo
|
</p>
|
||||||
</a>
|
<div class="tab-grid">
|
||||||
<p class="footer-note">Noo is an independent, unofficial Nextcloud client. Nextcloud is a trademark of Nextcloud GmbH.</p>
|
<div class="tab-chip"><span class="tab-chip-ic"><svg class="i" width="22" height="22"><use href="icons.svg#i-folder"/></svg></span><div><h3>Files</h3><p>Browse, upload, sort and filter every folder.</p></div></div>
|
||||||
<nav class="footer-links" aria-label="Footer">
|
<div class="tab-chip"><span class="tab-chip-ic"><svg class="i" width="22" height="22"><use href="icons.svg#i-photos"/></svg></span><div><h3>Photos</h3><p>Every image and video, grouped by month.</p></div></div>
|
||||||
<a href="https://github.com" target="_blank" rel="noopener">GitHub</a>
|
<div class="tab-chip"><span class="tab-chip-ic"><svg class="i" width="22" height="22"><use href="icons.svg#i-star"/></svg></span><div><h3>Favorites</h3><p>Everything you've starred, in one list.</p></div></div>
|
||||||
<a href="#privacy">Privacy</a>
|
<div class="tab-chip"><span class="tab-chip-ic"><svg class="i" width="22" height="22"><use href="icons.svg#i-activity"/></svg></span><div><h3>Activity</h3><p>What changed and who changed it.</p></div></div>
|
||||||
</nav>
|
<div class="tab-chip"><span class="tab-chip-ic"><svg class="i" width="22" height="22"><use href="icons.svg#i-trash"/></svg></span><div><h3>Trash</h3><p>Restore or permanently delete removed files.</p></div></div>
|
||||||
</div>
|
<div class="tab-chip"><span class="tab-chip-ic"><svg class="i" width="22" height="22"><use href="icons.svg#i-share"/></svg></span><div><h3>Shares</h3><p>What you've shared, and what's shared with you.</p></div></div>
|
||||||
</footer>
|
<div class="tab-chip"><span class="tab-chip-ic"><svg class="i" width="22" height="22"><use href="icons.svg#i-clock"/></svg></span><div><h3>Recent</h3><p>Recently edited and opened, grouped by day.</p></div></div>
|
||||||
|
<div class="tab-chip"><span class="tab-chip-ic"><svg class="i" width="22" height="22"><use href="icons.svg#i-offline"/></svg></span><div><h3>Offline</h3><p>Synced folders and files, no connection required.</p></div></div>
|
||||||
<script src="script.js"></script>
|
</div>
|
||||||
</body>
|
</div>
|
||||||
</html>
|
</section>
|
||||||
|
|
||||||
|
<!-- ============ PRIVACY ============ -->
|
||||||
|
<section class="section section-alt" id="privacy">
|
||||||
|
<div class="wrap privacy-grid">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Privacy</p>
|
||||||
|
<h2>It's your server. Noo just visits.</h2>
|
||||||
|
<p class="section-sub">
|
||||||
|
Noo never asks for your password. Signing in hands off to your
|
||||||
|
Nextcloud server's own login page (Login Flow v2), which returns
|
||||||
|
a revocable app token — not your credentials. The app talks to
|
||||||
|
your server and nowhere else, and collects nothing about you.
|
||||||
|
</p>
|
||||||
|
<p class="section-sub"><a class="text-link" href="privacy.html">Read the full privacy policy →</a></p>
|
||||||
|
</div>
|
||||||
|
<ul class="privacy-points">
|
||||||
|
<li><span class="pt-ic"><svg class="i" width="20" height="20"><use href="icons.svg#i-lock"/></svg></span> No password stored on device</li>
|
||||||
|
<li><span class="pt-ic"><svg class="i" width="20" height="20"><use href="icons.svg#i-server"/></svg></span> Talks only to your own server</li>
|
||||||
|
<li><span class="pt-ic"><svg class="i" width="20" height="20"><use href="icons.svg#i-eye"/></svg></span> No analytics, no trackers, no ads</li>
|
||||||
|
<li><span class="pt-ic"><svg class="i" width="20" height="20"><use href="icons.svg#i-database"/></svg></span> Your data never touches our servers — we don't have any</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ============ GET ============ -->
|
||||||
|
<section class="section section-cta" id="get">
|
||||||
|
<div class="wrap cta-inner">
|
||||||
|
<h2>Bring your Nextcloud with you.</h2>
|
||||||
|
<p class="section-sub">Free. Point it at your server and go.</p>
|
||||||
|
<div class="hero-actions cta-actions">
|
||||||
|
<a class="btn btn-primary btn-lg" href="#">Download for Android</a>
|
||||||
|
</div>
|
||||||
|
<ul class="soon-list" aria-label="Coming soon">
|
||||||
|
<li>macOS <span>coming soon</span></li>
|
||||||
|
<li>Windows <span>coming soon</span></li>
|
||||||
|
<li>iOS <span>coming soon</span></li>
|
||||||
|
</ul>
|
||||||
|
<p class="oss-note">
|
||||||
|
<svg class="i" width="20" height="20" aria-hidden="true"><use href="icons.svg#i-external"/></svg>
|
||||||
|
<span>Noo is open source. Read the code, file an issue, or build it yourself on
|
||||||
|
<!-- TODO: set the real repository URL -->
|
||||||
|
<a class="text-link" href="#" id="githubLink">GitHub</a>.</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="site-footer">
|
||||||
|
<div class="wrap footer-row">
|
||||||
|
<a class="wordmark wordmark-sm" href="#top">
|
||||||
|
<span class="wordmark-mark" aria-hidden="true"></span>
|
||||||
|
noo
|
||||||
|
</a>
|
||||||
|
<p class="footer-note">Noo is an independent, unofficial Nextcloud client. Nextcloud is a trademark of Nextcloud GmbH.</p>
|
||||||
|
<nav class="footer-links" aria-label="Footer">
|
||||||
|
<a href="#" class="github-link">GitHub</a>
|
||||||
|
<a href="privacy.html">Privacy policy</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
@@ -0,0 +1,175 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Privacy Policy — Noo</title>
|
||||||
|
<meta name="description" content="Noo connects only to the Nextcloud server you choose. No analytics, no advertising, no third-party services, and no collection of your data.">
|
||||||
|
<meta name="theme-color" content="#F3EFE6">
|
||||||
|
<link rel="icon" href="favicon.svg" type="image/svg+xml">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600;700&family=Schibsted+Grotesk:wght@500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
</head>
|
||||||
|
<body id="top">
|
||||||
|
|
||||||
|
<a class="skip-link" href="#main">Skip to content</a>
|
||||||
|
|
||||||
|
<header class="site-header">
|
||||||
|
<div class="wrap header-row">
|
||||||
|
<a class="wordmark" href="index.html">
|
||||||
|
<span class="wordmark-mark" aria-hidden="true"></span>
|
||||||
|
noo
|
||||||
|
</a>
|
||||||
|
<nav class="site-nav" aria-label="Primary">
|
||||||
|
<a href="index.html#features">Features</a>
|
||||||
|
<a href="index.html#customize">Customize</a>
|
||||||
|
<a href="index.html#tabs">Tabs</a>
|
||||||
|
<a href="privacy.html" aria-current="page">Privacy</a>
|
||||||
|
</nav>
|
||||||
|
<a class="btn btn-primary btn-sm" href="index.html#get">Get Noo</a>
|
||||||
|
<button class="nav-toggle" id="navToggle" aria-label="Open menu" aria-expanded="false">
|
||||||
|
<span></span><span></span><span></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<nav class="mobile-nav" id="mobileNav" aria-label="Primary">
|
||||||
|
<a href="index.html#features">Features</a>
|
||||||
|
<a href="index.html#customize">Customize</a>
|
||||||
|
<a href="index.html#tabs">Tabs</a>
|
||||||
|
<a href="privacy.html">Privacy</a>
|
||||||
|
<a class="btn btn-primary btn-sm" href="index.html#get">Get Noo</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main id="main" class="doc">
|
||||||
|
<div class="doc-wrap">
|
||||||
|
<p class="eyebrow">Legal</p>
|
||||||
|
<h1>Privacy Policy</h1>
|
||||||
|
<p class="doc-meta">Effective date: 1 October 2026 · Applies to the Noo mobile application and to this website</p>
|
||||||
|
|
||||||
|
<p class="doc-lead"><strong>In short:</strong> Noo talks to one place only — the Nextcloud server you tell it to use. We do not operate any servers that receive your data, we do not use analytics, advertising, crash-reporting or tracking services, and we do not collect, store, sell, share or have access to your personal information.</p>
|
||||||
|
|
||||||
|
<nav class="toc" aria-label="Contents">
|
||||||
|
<ol>
|
||||||
|
<li><a href="#definitions">Definitions</a></li>
|
||||||
|
<li><a href="#no-collection">We do not collect your data</a></li>
|
||||||
|
<li><a href="#your-server">Your Nextcloud server</a></li>
|
||||||
|
<li><a href="#no-third-parties">No third-party services</a></li>
|
||||||
|
<li><a href="#on-device">Data stored on your device</a></li>
|
||||||
|
<li><a href="#authentication">Authentication and app lock</a></li>
|
||||||
|
<li><a href="#permissions">Permissions</a></li>
|
||||||
|
<li><a href="#sharing">Sharing and file handoff</a></li>
|
||||||
|
<li><a href="#website">This website</a></li>
|
||||||
|
<li><a href="#children">Children</a></li>
|
||||||
|
<li><a href="#rights">Your rights</a></li>
|
||||||
|
<li><a href="#changes">Changes to this policy</a></li>
|
||||||
|
<li><a href="#contact">Contact</a></li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<h2 id="definitions">1. Definitions</h2>
|
||||||
|
<p>In this policy, <strong>“Noo”</strong>, <strong>“the App”</strong> means the Noo Nextcloud client application for Android and any other platform on which it is published; <strong>“we”</strong>, <strong>“us”</strong> and <strong>“the developer”</strong> mean the individual developer of the App; <strong>“you”</strong> means the person using the App; <strong>“Your Server”</strong> means the Nextcloud instance whose address you enter into the App, whether operated by you or by another party of your choosing; and <strong>“Personal Data”</strong> has the meaning given in applicable data-protection law, including Regulation (EU) 2016/679 (GDPR), the UK GDPR and the California Consumer Privacy Act, to the extent they apply.</p>
|
||||||
|
|
||||||
|
<h2 id="no-collection">2. We do not collect your data</h2>
|
||||||
|
<p>The developer does not collect, receive, record, store, transmit, process, sell, rent, license, disclose or otherwise have access to any Personal Data or any other information about you or your use of the App. The App contains no code that sends information to the developer or to any server operated by the developer. Because we hold no such data, we are not a controller or a processor of your Personal Data in respect of the App, and there is no data we are able to disclose, delete, correct or hand over to any third party, including in response to a legal request.</p>
|
||||||
|
<p>Specifically, the App does <strong>not</strong> contain or use:</p>
|
||||||
|
<ul>
|
||||||
|
<li>analytics, telemetry, usage statistics or behavioural measurement of any kind;</li>
|
||||||
|
<li>advertising networks, advertising identifiers or attribution services;</li>
|
||||||
|
<li>crash-reporting or error-reporting services;</li>
|
||||||
|
<li>social-media, marketing, A/B-testing or “growth” SDKs;</li>
|
||||||
|
<li>fingerprinting, cross-app tracking or profiling;</li>
|
||||||
|
<li>in-app purchases, subscriptions or accounts with the developer.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2 id="your-server">3. Your Nextcloud server</h2>
|
||||||
|
<p>The App is a client. Its only network communication is with Your Server, at the address you provide, in order to perform the actions you ask for: signing in, listing and transferring files, showing activity, managing shares and trash, and synchronising files you have chosen to keep available offline. This communication uses the standard Nextcloud and WebDAV interfaces and goes directly from your device to Your Server; it does not pass through the developer or any intermediary operated by the developer.</p>
|
||||||
|
<p>What Your Server logs, stores and does with that traffic is governed by the privacy policy and practices of whoever operates Your Server. We have no control over, and accept no responsibility for, those practices. If you use a server run by someone else, please consult them.</p>
|
||||||
|
<p>The App permits unencrypted (HTTP) connections so that you can reach servers on a private network. Whether to use HTTP or HTTPS is your choice; we strongly recommend HTTPS for any server reachable over the internet, because unencrypted traffic can be read by others on the network.</p>
|
||||||
|
<p>If you add several accounts, the App communicates with each account’s server only when you use that account, and never shares information between them.</p>
|
||||||
|
|
||||||
|
<h2 id="no-third-parties">4. No third-party services</h2>
|
||||||
|
<p>The App does not connect to any service other than Your Server. In particular:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Fonts and assets.</strong> The typefaces used by the App are bundled inside the App package. The App does not download fonts, images, icons, configuration or code from any third-party host at runtime.</li>
|
||||||
|
<li><strong>No remote configuration.</strong> The App does not fetch feature flags, update manifests or any other instructions from the developer.</li>
|
||||||
|
<li><strong>No embedded web content from others.</strong> Sign-in uses your device’s browser to open the login page of Your Server (see section 6); no third-party web content is loaded.</li>
|
||||||
|
<li><strong>Open-source libraries.</strong> The App is built with open-source libraries that run locally on your device. They are not permitted by us to, and to our knowledge do not, transmit data to their authors.</li>
|
||||||
|
</ul>
|
||||||
|
<p>If the App is distributed through an app store (for example Google Play), the store operator may collect information about your download and installation under its own policy. That collection is independent of the App and outside our control.</p>
|
||||||
|
|
||||||
|
<h2 id="on-device">5. Data stored on your device</h2>
|
||||||
|
<p>To work, the App keeps some information locally on your device. This information remains on your device, is not transmitted to the developer, and is removed when you remove the account, clear the App’s storage or uninstall the App.</p>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Data</th><th>Purpose</th><th>Where it lives</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Server address, username and an app-specific access token for each account</td><td>Keep you signed in</td><td>The device’s secure credential storage (Android Keystore-backed storage)</td></tr>
|
||||||
|
<tr><td>Preferences: theme, accent colour, bottom bar, tab order, action bar, swipe actions, lock and sync settings</td><td>Remember how you set the App up</td><td>App-private storage</td></tr>
|
||||||
|
<tr><td>Cached file lists, thumbnails and previews</td><td>Speed and offline browsing</td><td>App-private cache</td></tr>
|
||||||
|
<tr><td>Files you marked for offline use and files you download</td><td>Offline access; saving to your device</td><td>App-private storage, or your device’s Downloads folder when you save a file there</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p>The App does not read your contacts, location, calendar, messages, call history, microphone or camera, and it does not scan or index your device’s other files. It reads a local file only when you pick it to upload or share it to the App.</p>
|
||||||
|
|
||||||
|
<h2 id="authentication">6. Authentication and app lock</h2>
|
||||||
|
<h3>Sign-in</h3>
|
||||||
|
<p>Noo signs in using Nextcloud’s Login Flow v2. You enter only a server address; you authenticate on Your Server’s own web page in your browser, and Your Server issues the App an app-specific token. <strong>The App never sees, collects or stores your account password</strong>, and you can revoke the token at any time from the security settings of Your Server.</p>
|
||||||
|
<h3>Biometric and device lock</h3>
|
||||||
|
<p>The optional login lock, hidden-file lock and account-switch lock use your device’s own authentication (fingerprint, face, PIN, pattern or password) through the operating system. The operating system performs the check and tells the App only whether it succeeded. The App does not receive, store or transmit your biometric data, your PIN or your device credentials.</p>
|
||||||
|
|
||||||
|
<h2 id="permissions">7. Permissions</h2>
|
||||||
|
<p>On Android the App requests only the permissions needed for the features you use:</p>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Permission</th><th>Why</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Internet</td><td>To communicate with Your Server.</td></tr>
|
||||||
|
<tr><td>Network state</td><td>To know whether you are offline, so the App can avoid pointless requests and show offline files.</td></tr>
|
||||||
|
<tr><td>Biometric</td><td>To support the optional device-based lock.</td></tr>
|
||||||
|
<tr><td>Foreground service (data sync) and Notifications</td><td>To keep an upload, download or sync running, and to show its progress with a cancel option, when the App is not on screen.</td></tr>
|
||||||
|
<tr><td>Write external storage (Android 9 and earlier only)</td><td>To save downloads on old devices that require it.</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p>You can withhold or revoke any permission in your system settings; features that depend on it may stop working.</p>
|
||||||
|
|
||||||
|
<h2 id="sharing">8. Sharing and file handoff</h2>
|
||||||
|
<p>When you use Noo’s sharing features, such as creating a share link, adding a person to a share, or sending a file directly, that action is carried out on Your Server. Anyone you share with can then access the content under Your Server’s rules. When you hand a file to another app through the system share sheet or file picker, that other app receives the file and is governed by its own privacy policy. You are responsible for what you choose to share and with whom.</p>
|
||||||
|
|
||||||
|
<h2 id="website">9. This website</h2>
|
||||||
|
<p>This website (<strong>noo.ayushya.dev</strong>) is separate from the App. It is a static site: it has no accounts, forms, cookies, analytics or advertising, and it does not set tracking identifiers. The interactive demos run entirely in your browser and send nothing anywhere; any preferences you toggle in them are not stored.</p>
|
||||||
|
<p>To display its typefaces, the website loads fonts from Google Fonts (fonts.googleapis.com and fonts.gstatic.com). Your browser therefore sends your IP address and standard request headers to Google when you visit, subject to Google’s own privacy policy. The site is hosted on Cloudflare, which, like any web host, processes technical request data (such as IP address and user agent) to deliver pages and protect against abuse, under its own policy. We do not receive this data in any identifiable form beyond what the host’s standard dashboard may aggregate. None of this applies to the App.</p>
|
||||||
|
|
||||||
|
<h2 id="children">10. Children</h2>
|
||||||
|
<p>The App is a general-purpose file client and is not directed at children. Because we collect no Personal Data from anyone, we also collect none from children. Accounts on Your Server, and any age requirements that apply to them, are determined by whoever operates Your Server.</p>
|
||||||
|
|
||||||
|
<h2 id="rights">11. Your rights</h2>
|
||||||
|
<p>Depending on where you live, you may have rights over your Personal Data, such as access, correction, deletion, portability, restriction and objection, and the right to complain to a supervisory authority. Since we hold no Personal Data about you, there is nothing for us to provide or erase; to exercise these rights over data held on Your Server, contact whoever operates it. You are always in control of data on your own device: you can delete any of it by removing an account, clearing the App’s storage, or uninstalling the App.</p>
|
||||||
|
<p>We do not sell or share Personal Data, as those terms are defined by the California Consumer Privacy Act, and we do not engage in targeted advertising or automated decision-making.</p>
|
||||||
|
|
||||||
|
<h2 id="changes">12. Changes to this policy</h2>
|
||||||
|
<p>If the App changes in a way that affects this policy, for example if it ever needed to connect to a service other than Your Server, we will update this page and its effective date before that change is released, and we will describe the change in the App’s release notes. Continued use of the App after an update means you accept the revised policy. Previous versions are available on request.</p>
|
||||||
|
|
||||||
|
<h2 id="contact">13. Contact</h2>
|
||||||
|
<p>Questions about this policy can be sent to the developer through the contact details published at <a href="https://ayushya.dev">ayushya.dev</a>.</p>
|
||||||
|
|
||||||
|
<p class="doc-meta" style="margin-top:40px">Noo is an independent, unofficial client and is not affiliated with or endorsed by Nextcloud GmbH. Nextcloud is a trademark of Nextcloud GmbH.</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="site-footer">
|
||||||
|
<div class="wrap footer-row">
|
||||||
|
<a class="wordmark wordmark-sm" href="index.html">
|
||||||
|
<span class="wordmark-mark" aria-hidden="true"></span>
|
||||||
|
noo
|
||||||
|
</a>
|
||||||
|
<p class="footer-note">Noo is an independent, unofficial Nextcloud client. Nextcloud is a trademark of Nextcloud GmbH.</p>
|
||||||
|
<nav class="footer-links" aria-label="Footer">
|
||||||
|
<a href="#" class="github-link">GitHub</a>
|
||||||
|
<a href="privacy.html">Privacy policy</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+790
-141
@@ -1,141 +1,790 @@
|
|||||||
(() => {
|
(() => {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
/* ---------- mobile nav ---------- */
|
const $ = (s, r = document) => r.querySelector(s);
|
||||||
|
const $$ = (s, r = document) => Array.from(r.querySelectorAll(s));
|
||||||
const navToggle = document.getElementById("navToggle");
|
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||||
const mobileNav = document.getElementById("mobileNav");
|
const darkQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
if (navToggle && mobileNav) {
|
|
||||||
navToggle.addEventListener("click", () => {
|
/* Lucide icons, drawn from icons.svg (generated from the app's own design
|
||||||
const open = mobileNav.classList.toggle("is-open");
|
kit) so every glyph in a mockup is the glyph the app ships. */
|
||||||
navToggle.setAttribute("aria-expanded", String(open));
|
const ic = (name, size = 20, cls = "") =>
|
||||||
navToggle.setAttribute("aria-label", open ? "Close menu" : "Open menu");
|
`<svg class="i ${cls}" width="${size}" height="${size}" aria-hidden="true"><use href="icons.svg#i-${name}"/></svg>`;
|
||||||
});
|
|
||||||
mobileNav.querySelectorAll("a").forEach((a) =>
|
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
||||||
a.addEventListener("click", () => {
|
|
||||||
mobileNav.classList.remove("is-open");
|
/* ---------------------------------------------------------------------
|
||||||
navToggle.setAttribute("aria-expanded", "false");
|
Tokens - lib/theme/design_tokens.dart, light + dark, plus the accent
|
||||||
})
|
presets from AppTheme.seedColors with the exact tonal values
|
||||||
);
|
NooColors.fromSeed produces for each.
|
||||||
}
|
--------------------------------------------------------------------- */
|
||||||
|
|
||||||
/* ---------- exploded hero parallax ---------- */
|
const PALETTE = {
|
||||||
/* Each [data-depth] layer drifts at its own rate on scroll, and tilts
|
light: {
|
||||||
gently toward the pointer - the "exploded" pieces read as separate
|
bg: "#F3EFE6", sf: "#FFFFFF", sf2: "#F6F5F2", sf3: "#EAE8E2", ln: "#EAE8E2",
|
||||||
depths rather than a flat collage. Skipped entirely under
|
fg1: "#0E0D0B", fg2: "#5A5852", fg3: "#7C7A72",
|
||||||
prefers-reduced-motion. */
|
dg: "#A8202A", dgs: "#FDE7E7", dgf: "#A8202A",
|
||||||
|
ok: "#3E7A23", oks: "#EAF5DF", wn: "#8C5A0E", wns: "#FBEFD0", in: "#2940A8", ins: "#E6ECFE",
|
||||||
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
scrim: "rgba(14,13,11,0.45)",
|
||||||
const heroVisual = document.getElementById("heroVisual");
|
},
|
||||||
|
dark: {
|
||||||
if (heroVisual && !reduceMotion) {
|
bg: "#0E0D0B", sf: "#25241F", sf2: "#3D3C38", sf3: "#5A5852", ln: "#3D3C38",
|
||||||
const layers = Array.from(heroVisual.querySelectorAll("[data-depth]")).map((el) => ({
|
fg1: "#F3EFE6", fg2: "#BDBAB1", fg3: "#9E9B92",
|
||||||
el,
|
dg: "#F49E9E", dgs: "#560F18", dgf: "#A8202A",
|
||||||
depth: parseFloat(el.dataset.depth) || 0.3,
|
ok: "#B7DD9F", oks: "#1E3F10", wn: "#F0C26C", wns: "#4A3208", in: "#B6C3FB", ins: "#1C2B73",
|
||||||
}));
|
scrim: "rgba(0,0,0,0.6)",
|
||||||
|
},
|
||||||
let scrollOffset = 0;
|
};
|
||||||
let pointer = { x: 0, y: 0 }; // -1..1
|
|
||||||
let ticking = false;
|
const ACCENTS = [
|
||||||
|
{ id: "violet", name: "Default", c: "#8D0DE3", light: ["#8D0DE3", "#ECDFFF"], dark: ["#CBA5FD", "#2F0454"] },
|
||||||
const apply = () => {
|
{ id: "blue", name: "Nextcloud Blue", c: "#0082C9", light: ["#0082C9", "#CEE5FF"], dark: ["#9BCBFB", "#0D4A73"] },
|
||||||
const rect = heroVisual.getBoundingClientRect();
|
{ id: "teal", name: "Ocean Teal", c: "#009688", light: ["#009688", "#9EF2E4"], dark: ["#82D5C8", "#005048"] },
|
||||||
const inView = rect.bottom > 0 && rect.top < window.innerHeight;
|
{ id: "purple", name: "Deep Purple", c: "#6750A4", light: ["#6750A4", "#E9DDFF"], dark: ["#CFBDFE", "#4D3D75"] },
|
||||||
if (inView) {
|
{ id: "amber", name: "Sunset Amber", c: "#E65100", light: ["#E65100", "#FFDBCF"], dark: ["#FFB59A", "#71361E"] },
|
||||||
for (const layer of layers) {
|
{ id: "green", name: "Emerald Green", c: "#2E7D32", light: ["#2E7D32", "#BCF0B4"], dark: ["#A1D39A", "#245024"] },
|
||||||
const scrollShift = scrollOffset * layer.depth * 40;
|
// Material You: follows the OS wallpaper, so this is only a sample.
|
||||||
const px = pointer.x * layer.depth * 14;
|
{ id: "wallpaper", name: "Match wallpaper", c: "#8E4956", light: ["#8E4956", "#FFD9DE"], dark: ["#FFB1BD", "#72333F"], wallpaper: true },
|
||||||
const py = pointer.y * layer.depth * 14;
|
];
|
||||||
layer.el.style.transform =
|
|
||||||
`translate3d(${px}px, ${scrollShift + py}px, 0)`;
|
function resolveVars(s) {
|
||||||
}
|
const mode = s.theme === "system" ? (darkQuery.matches ? "dark" : "light") : s.theme;
|
||||||
}
|
const p = { ...PALETTE[mode] };
|
||||||
ticking = false;
|
const a = ACCENTS.find((x) => x.id === s.accent) || ACCENTS[0];
|
||||||
};
|
const [text, soft] = a[mode];
|
||||||
|
if (mode === "dark" && s.amoled) p.bg = p.sf = p.sf2 = p.sf3 = "#000000";
|
||||||
const requestTick = () => {
|
const v = { ...p, ac: a.c, act: text, acs: soft };
|
||||||
if (!ticking) {
|
return { mode, css: Object.entries(v).map(([k, val]) => `--${k}:${val}`).join(";") };
|
||||||
ticking = true;
|
}
|
||||||
requestAnimationFrame(apply);
|
|
||||||
}
|
/* ---------------------------------------------------------------------
|
||||||
};
|
Mock content
|
||||||
|
--------------------------------------------------------------------- */
|
||||||
const onScroll = () => {
|
|
||||||
const rect = heroVisual.getBoundingClientRect();
|
const TABS = {
|
||||||
// -1 (scrolled well past) .. 1 (below viewport) - 0 while centered.
|
files: { label: "Files", icon: "folder", blurb: "Browse, upload, sort and filter every folder." },
|
||||||
scrollOffset = (window.innerHeight / 2 - (rect.top + rect.height / 2)) / window.innerHeight;
|
photos: { label: "Photos", icon: "photos", blurb: "Every image and video, grouped by month." },
|
||||||
requestTick();
|
favorites: { label: "Favorites", icon: "star", blurb: "Everything you've starred, in one list." },
|
||||||
};
|
activity: { label: "Activity", icon: "activity", blurb: "What changed and who changed it." },
|
||||||
|
trash: { label: "Trash", icon: "trash", blurb: "Restore or permanently delete removed files." },
|
||||||
const onPointerMove = (e) => {
|
shares: { label: "Shares", icon: "share", blurb: "What you've shared and what's shared with you." },
|
||||||
const rect = heroVisual.getBoundingClientRect();
|
recent: { label: "Recent", icon: "clock", blurb: "Recently edited and opened, by day." },
|
||||||
pointer.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
offline: { label: "Offline", icon: "offline", blurb: "Synced folders and files, available without a connection." },
|
||||||
pointer.y = ((e.clientY - rect.top) / rect.height) * 2 - 1;
|
};
|
||||||
requestTick();
|
const TAB_ORDER_DEFAULT = ["files", "photos", "favorites", "offline", "shares", "recent", "activity", "trash"];
|
||||||
};
|
const PINNED_DEFAULT = ["files", "photos", "favorites", "offline", "shares"];
|
||||||
|
|
||||||
const onPointerLeave = () => {
|
const KIND_ICON = { folder: "folder", pdf: "doc", doc: "doc", sheet: "sheet", video: "film", image: "image", zip: "zip" };
|
||||||
pointer = { x: 0, y: 0 };
|
const KIND_VARS = {
|
||||||
requestTick();
|
folder: ["acs", "act"], pdf: ["dgs", "dg"], doc: ["ins", "in"], image: ["ins", "in"],
|
||||||
};
|
sheet: ["oks", "ok"], video: ["wns", "wn"], zip: ["sf3", "fg2"],
|
||||||
|
};
|
||||||
window.addEventListener("scroll", onScroll, { passive: true });
|
|
||||||
heroVisual.addEventListener("pointermove", onPointerMove);
|
// [name, kind, meta, flags]
|
||||||
heroVisual.addEventListener("pointerleave", onPointerLeave);
|
const ROWS = {
|
||||||
onScroll();
|
files: [
|
||||||
}
|
["Documents", "folder", "24 items · Sep 20"],
|
||||||
|
["Photos", "folder", "1,204 items · Today"],
|
||||||
/* ---------- accent color demo ---------- */
|
["Projects", "folder", "8 items · Sep 19", "shared"],
|
||||||
|
["Q3 Planning.pdf", "pdf", "2.4 MB · Yesterday", "fav ok"],
|
||||||
const accentDemo = document.getElementById("accentDemo");
|
["Budget 2026.xlsx", "sheet", "186 KB · Sep 18", "ok"],
|
||||||
if (accentDemo) {
|
["Offsite recap.mp4", "video", "412 MB · Sep 14"],
|
||||||
const buttons = accentDemo.querySelectorAll(".swatch-btn");
|
["Whiteboard.jpg", "image", "3.1 MB · Sep 12"],
|
||||||
buttons.forEach((btn) => {
|
["Brand brief.docx", "doc", "58 KB · Sep 10"],
|
||||||
btn.addEventListener("click", () => {
|
],
|
||||||
buttons.forEach((b) => b.classList.remove("is-active"));
|
favorites: [
|
||||||
btn.classList.add("is-active");
|
["Q3 Planning.pdf", "pdf", "2.4 MB · Yesterday", "ok"],
|
||||||
const color = btn.dataset.accent;
|
["Projects", "folder", "8 items · Sep 19", "shared"],
|
||||||
accentDemo.style.setProperty("--demo-accent", color);
|
["Passport scan.pdf", "pdf", "1.1 MB · Aug 30", "ok"],
|
||||||
});
|
["Family budget.xlsx", "sheet", "92 KB · Aug 21", "shared"],
|
||||||
});
|
["Wedding", "folder", "212 items · Jun 14"],
|
||||||
}
|
],
|
||||||
|
offline: [
|
||||||
/* ---------- bottom bar style demo ---------- */
|
["Q3 Planning.pdf", "pdf", "Up to date · 2.4 MB", "ok"],
|
||||||
|
["Offsite recap.mp4", "video", "Up to date · 412 MB", "ok"],
|
||||||
const barDemo = document.getElementById("barDemo");
|
["Contracts", "folder", "Syncing 12 of 18 files", "sync"],
|
||||||
const demoBar = document.getElementById("demoBar");
|
["Passport scan.pdf", "pdf", "Up to date · 1.1 MB", "ok"],
|
||||||
if (barDemo && demoBar) {
|
],
|
||||||
const buttons = barDemo.querySelectorAll(".segmented-btn");
|
shares: [
|
||||||
buttons.forEach((btn) => {
|
["Projects", "folder", "Ana Souza · Can edit", "shared"],
|
||||||
btn.addEventListener("click", () => {
|
["Offsite photos", "folder", "Leo Park · Can view", "shared"],
|
||||||
buttons.forEach((b) => b.classList.remove("is-active"));
|
["Q3 Planning.pdf", "pdf", "Ana Souza · Can edit", "shared"],
|
||||||
btn.classList.add("is-active");
|
],
|
||||||
demoBar.classList.toggle("is-attached", btn.dataset.bar === "attached");
|
recent: [
|
||||||
});
|
["Q3 Planning.pdf", "pdf", "Edited 10:24 · Documents", "fav"],
|
||||||
});
|
["Whiteboard.jpg", "image", "Uploaded 9:40 · Photos"],
|
||||||
}
|
["Brand brief.docx", "doc", "Opened 8:12 · Projects"],
|
||||||
|
["Budget 2026.xlsx", "sheet", "Edited yesterday · Documents"],
|
||||||
/* ---------- scroll reveal ---------- */
|
],
|
||||||
|
trash: [
|
||||||
const revealTargets = document.querySelectorAll(
|
["Old notes.txt", "doc", "Deleted today · /Documents"],
|
||||||
".card, .customize-demo, .customize-list, .privacy-points li"
|
["Draft v1.docx", "doc", "Deleted yesterday · /Projects"],
|
||||||
);
|
["Screenshots", "folder", "Deleted Sep 12 · /Photos"],
|
||||||
revealTargets.forEach((el) => el.classList.add("reveal"));
|
],
|
||||||
|
activity: [
|
||||||
if ("IntersectionObserver" in window && !reduceMotion) {
|
["Ana Souza shared Q3 Planning.pdf with you", "pdf", "10:24"],
|
||||||
const io = new IntersectionObserver(
|
["Leo Park edited Budget 2026.xlsx", "sheet", "9:02"],
|
||||||
(entries) => {
|
["You uploaded Whiteboard.jpg", "image", "Yesterday"],
|
||||||
for (const entry of entries) {
|
["Ana Souza commented on Projects", "folder", "Yesterday"],
|
||||||
if (entry.isIntersecting) {
|
],
|
||||||
entry.target.classList.add("is-visible");
|
};
|
||||||
io.unobserve(entry.target);
|
const PHOTO_TINTS = ["#8DCDE2", "#96B0FD", "#E5D8BD", "#EEEDB3", "#DA9AC7", "#A7D296", "#E9A6A7", "#85D1BD", "#CBA5FD"];
|
||||||
}
|
|
||||||
}
|
const avatar = (txt, tint, size = 36, current = false) =>
|
||||||
},
|
`<span class="av${current ? " av-current" : ""}" style="--s:${size}px;background:${tint}">${esc(txt)}</span>`;
|
||||||
{ threshold: 0.15, rootMargin: "0px 0px -40px 0px" }
|
|
||||||
);
|
const fileRow = (r) => {
|
||||||
revealTargets.forEach((el) => io.observe(el));
|
const [name, kind, meta, flags = ""] = r;
|
||||||
} else {
|
const [bg, fg] = KIND_VARS[kind];
|
||||||
revealTargets.forEach((el) => el.classList.add("is-visible"));
|
const f = flags.split(" ");
|
||||||
}
|
const badges =
|
||||||
})();
|
(f.includes("fav") ? ic("star", 16, "b-fav") : "") +
|
||||||
|
(f.includes("shared") ? ic("users", 16, "b-muted") : "") +
|
||||||
|
(f.includes("ok") ? ic("ok", 16, "b-ok") : "") +
|
||||||
|
(f.includes("sync") ? ic("sync", 16, "b-sync") : "");
|
||||||
|
return `<div class="fr"><span class="ft" style="background:var(--${bg});color:var(--${fg})">${ic(KIND_ICON[kind], 20)}</span>` +
|
||||||
|
`<span class="fm"><b>${esc(name)}</b><i>${esc(meta)}</i></span><span class="fb">${badges}</span></div>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------
|
||||||
|
The phone: top bar, body, bottom bar (attached / floating / search
|
||||||
|
satellite), drawer + avatar menu. Mirrors NooTopBar, NooBottomBar,
|
||||||
|
NooDrawer and the avatar menu in the Flutter app (Android layout).
|
||||||
|
--------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
function phoneState(over = {}) {
|
||||||
|
return {
|
||||||
|
theme: "light", accent: "violet", amoled: false,
|
||||||
|
bar: "floating", searchInBar: false, nav: "drawer",
|
||||||
|
order: [...TAB_ORDER_DEFAULT], pinned: new Set(PINNED_DEFAULT),
|
||||||
|
active: "files", overlay: null, ...over,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function visibleTabs(s) {
|
||||||
|
const max = s.searchInBar ? 4 : 5;
|
||||||
|
return s.order.filter((t) => s.pinned.has(t)).slice(0, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPhone(host, s) {
|
||||||
|
const { css } = resolveVars(s);
|
||||||
|
const tabs = visibleTabs(s);
|
||||||
|
if (!tabs.includes(s.active)) s.active = tabs[0] || "files";
|
||||||
|
const hidden = s.order.filter((t) => !tabs.includes(t));
|
||||||
|
const t = TABS[s.active];
|
||||||
|
const floating = s.bar === "floating";
|
||||||
|
|
||||||
|
const bodyRows =
|
||||||
|
s.active === "photos"
|
||||||
|
? `<div class="np-photos">${PHOTO_TINTS.concat(PHOTO_TINTS).slice(0, 12)
|
||||||
|
.map((c, i) => `<span style="background:${c};opacity:${0.65 + ((i * 7) % 4) * 0.1}"></span>`).join("")}</div>`
|
||||||
|
: `<div class="np-list">${(ROWS[s.active] || ROWS.files).map(fileRow).join("")}</div>`;
|
||||||
|
|
||||||
|
const searchLauncher = !s.searchInBar
|
||||||
|
? `<span class="np-search">${ic("search", 18)}<span>Search</span></span>`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const hamburger = s.nav === "drawer"
|
||||||
|
? `<button class="np-ib" data-act="menu" aria-label="Open menu">${ic("menu", 24)}</button>`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const barItem = (id) => {
|
||||||
|
const sel = id === s.active;
|
||||||
|
const d = TABS[id];
|
||||||
|
if (floating && !sel) {
|
||||||
|
return `<button class="nb nb-icon" data-tab="${id}" aria-label="${d.label}">${ic(d.icon, 25)}</button>`;
|
||||||
|
}
|
||||||
|
return `<button class="nb${sel ? " is-sel" : ""}" data-tab="${id}" aria-label="${d.label}">` +
|
||||||
|
`<span class="nb-pill">${ic(d.icon, 24)}</span><span class="nb-label">${d.label}</span></button>`;
|
||||||
|
};
|
||||||
|
const searchItem = !s.searchInBar ? "" : floating
|
||||||
|
? `<button class="nb-sat" data-act="search" aria-label="Search">${ic("search", 24)}</button>`
|
||||||
|
: "";
|
||||||
|
const attachedSearch = s.searchInBar && !floating
|
||||||
|
? `<button class="nb nb-search" data-act="search" aria-label="Search"><span class="nb-pill">${ic("search", 24)}</span><span class="nb-label" style="opacity:0">Search</span></button>`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const bar = `<footer class="np-bar ${floating ? "is-floating" : "is-attached"}">` +
|
||||||
|
`<div class="np-bar-row">${tabs.map(barItem).join("")}${attachedSearch}</div>${searchItem}</footer>`;
|
||||||
|
|
||||||
|
const fab = s.active === "files"
|
||||||
|
? `<button class="np-fab" style="bottom:${floating ? 88 : 96}px" aria-label="Create">${ic("plus", 24)}</button>`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
let overlay = "";
|
||||||
|
if (s.overlay === "drawer") {
|
||||||
|
overlay = `<div class="np-scrim" data-act="close"></div><nav class="np-drawer" aria-label="Navigation">` +
|
||||||
|
`<div class="np-drawer-head"><span class="np-mark"></span><b>noo</b></div>` +
|
||||||
|
hidden.map((id) => `<button class="np-dr" data-tab="${id}">${ic(TABS[id].icon, 20)}<span>${TABS[id].label}</span></button>`).join("") +
|
||||||
|
`<div class="np-sep"></div><button class="np-dr">${ic("settings", 20)}<span>Settings</span></button></nav>`;
|
||||||
|
} else if (s.overlay === "avatar") {
|
||||||
|
overlay = `<div class="np-scrim" data-act="close"></div><div class="np-pop" role="menu">` +
|
||||||
|
`<div class="np-acct">${avatar("A", "#DA9AC7", 32, true)}<span><b>ayushya</b><i>cloud.example.com</i></span>${ic("check", 18, "b-sync")}</div>` +
|
||||||
|
`<div class="np-acct">${avatar("D", "#8DCDE2", 32)}<span><b>ayushya.dev</b><i>drive.ayushya.dev</i></span></div>` +
|
||||||
|
`<div class="np-sep"></div>` +
|
||||||
|
hidden.map((id) => `<button class="np-dr" data-tab="${id}">${ic(TABS[id].icon, 20)}<span>${TABS[id].label}</span></button>`).join("") +
|
||||||
|
`<div class="np-sep"></div><button class="np-dr">${ic("settings", 20)}<span>Settings</span></button></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
host.innerHTML =
|
||||||
|
`<div class="np" style="${css}">` +
|
||||||
|
`<div class="np-status"><span>9:41</span><span class="np-status-i">${ic("signal", 14)}${ic("wifi", 14)}${ic("battery", 16)}</span></div>` +
|
||||||
|
`<header class="np-top">${hamburger}<h4>${t.label}</h4>${searchLauncher}` +
|
||||||
|
`<button class="np-avbtn" data-act="avatar" aria-label="${s.nav === "avatar" ? "Menu" : "Accounts"}">${avatar("A", "#DA9AC7", 36, true)}</button></header>` +
|
||||||
|
`<div class="np-body">${bodyRows}</div>${fab}${bar}${overlay}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mountPhone(host, state) {
|
||||||
|
const rerender = () => renderPhone(host, state);
|
||||||
|
host.addEventListener("click", (e) => {
|
||||||
|
const tabBtn = e.target.closest("[data-tab]");
|
||||||
|
const act = e.target.closest("[data-act]");
|
||||||
|
if (tabBtn) {
|
||||||
|
state.active = tabBtn.dataset.tab;
|
||||||
|
state.overlay = null;
|
||||||
|
rerender();
|
||||||
|
} else if (act) {
|
||||||
|
const a = act.dataset.act;
|
||||||
|
if (a === "menu") state.overlay = "drawer";
|
||||||
|
else if (a === "avatar") state.overlay = state.nav === "avatar" ? "avatar" : null;
|
||||||
|
else if (a === "close") state.overlay = null;
|
||||||
|
rerender();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
rerender();
|
||||||
|
return rerender;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------
|
||||||
|
Hero phone
|
||||||
|
--------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
const heroPhone = $("#heroPhone");
|
||||||
|
if (heroPhone) {
|
||||||
|
const heroState = phoneState({ theme: "system", bar: "floating" });
|
||||||
|
const r = mountPhone(heroPhone, heroState);
|
||||||
|
darkQuery.addEventListener?.("change", r);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------
|
||||||
|
Appearance lab
|
||||||
|
--------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
const lab = $("#labPhone");
|
||||||
|
if (lab) {
|
||||||
|
const S = phoneState({ theme: "system" });
|
||||||
|
const rerender = mountPhone(lab, S);
|
||||||
|
darkQuery.addEventListener?.("change", rerender);
|
||||||
|
|
||||||
|
// Accent swatches
|
||||||
|
const sw = $("#labAccents");
|
||||||
|
sw.innerHTML = ACCENTS.map((a) =>
|
||||||
|
`<button class="swatch-btn${a.id === S.accent ? " is-active" : ""}" data-accent="${a.id}" aria-label="${a.name}" title="${a.name}" style="--c:${a.c}">${a.wallpaper ? ic("wallpaper", 16) : ""}</button>`
|
||||||
|
).join("");
|
||||||
|
const accName = $("#labAccentName");
|
||||||
|
const syncAccent = () => {
|
||||||
|
$$(".swatch-btn", sw).forEach((b) => b.classList.toggle("is-active", b.dataset.accent === S.accent));
|
||||||
|
const a = ACCENTS.find((x) => x.id === S.accent);
|
||||||
|
accName.textContent = a.wallpaper ? "Match wallpaper (sample colour shown)" : a.name;
|
||||||
|
};
|
||||||
|
sw.addEventListener("click", (e) => {
|
||||||
|
const b = e.target.closest("[data-accent]");
|
||||||
|
if (!b) return;
|
||||||
|
S.accent = b.dataset.accent;
|
||||||
|
syncAccent();
|
||||||
|
rerender();
|
||||||
|
});
|
||||||
|
syncAccent();
|
||||||
|
|
||||||
|
// Segmented controls: data-lab="theme|bar|nav" data-val="..."
|
||||||
|
$$("[data-lab]").forEach((group) => {
|
||||||
|
const key = group.dataset.lab;
|
||||||
|
const sync = () => $$("[data-val]", group).forEach((b) => b.classList.toggle("is-active", String(S[key]) === b.dataset.val));
|
||||||
|
group.addEventListener("click", (e) => {
|
||||||
|
const b = e.target.closest("[data-val]");
|
||||||
|
if (!b) return;
|
||||||
|
S[key] = b.dataset.val;
|
||||||
|
S.overlay = null;
|
||||||
|
sync();
|
||||||
|
rerender();
|
||||||
|
updateAmoled();
|
||||||
|
tabsRender();
|
||||||
|
});
|
||||||
|
sync();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggles: data-toggle="amoled|searchInBar"
|
||||||
|
const amoledRow = $("#amoledRow");
|
||||||
|
const resolvedMode = () => resolveVars(S).mode;
|
||||||
|
function updateAmoled() {
|
||||||
|
const isDark = resolvedMode() === "dark";
|
||||||
|
amoledRow.classList.toggle("is-disabled", !isDark);
|
||||||
|
$("[data-toggle=amoled]").disabled = !isDark;
|
||||||
|
}
|
||||||
|
$$("[data-toggle]").forEach((t) => {
|
||||||
|
const key = t.dataset.toggle;
|
||||||
|
t.setAttribute("role", "switch");
|
||||||
|
const sync = () => {
|
||||||
|
t.classList.toggle("is-on", !!S[key]);
|
||||||
|
t.setAttribute("aria-checked", String(!!S[key]));
|
||||||
|
};
|
||||||
|
t.addEventListener("click", () => {
|
||||||
|
S[key] = !S[key];
|
||||||
|
sync();
|
||||||
|
rerender();
|
||||||
|
tabsRender();
|
||||||
|
});
|
||||||
|
sync();
|
||||||
|
});
|
||||||
|
darkQuery.addEventListener?.("change", updateAmoled);
|
||||||
|
updateAmoled();
|
||||||
|
|
||||||
|
// Tabs: reorder + pin
|
||||||
|
const tabsList = $("#labTabs");
|
||||||
|
const tabsNote = $("#labTabsNote");
|
||||||
|
function tabsRender() {
|
||||||
|
const max = S.searchInBar ? 4 : 5;
|
||||||
|
tabsNote.textContent = `Drag to reorder, pin up to ${max} to the bottom bar` + (S.searchInBar ? " (Search takes the fifth slot)" : "");
|
||||||
|
tabsList.innerHTML = S.order.map((id) =>
|
||||||
|
`<li class="tab-row" data-id="${id}"><span class="grip" data-grip aria-label="Drag to reorder">${ic("grip", 18)}</span>` +
|
||||||
|
`<span class="tab-ic">${ic(TABS[id].icon, 20)}</span><span class="tab-label">${TABS[id].label}</span>` +
|
||||||
|
`<button class="toggle${S.pinned.has(id) ? " is-on" : ""}" role="switch" aria-checked="${S.pinned.has(id)}" aria-label="Pin ${TABS[id].label}" data-pin="${id}"><i></i></button></li>`
|
||||||
|
).join("");
|
||||||
|
}
|
||||||
|
tabsList.addEventListener("click", (e) => {
|
||||||
|
const b = e.target.closest("[data-pin]");
|
||||||
|
if (!b) return;
|
||||||
|
const id = b.dataset.pin;
|
||||||
|
const max = S.searchInBar ? 4 : 5;
|
||||||
|
if (S.pinned.has(id)) {
|
||||||
|
if (S.pinned.size <= 1) return;
|
||||||
|
S.pinned.delete(id);
|
||||||
|
} else {
|
||||||
|
if (S.pinned.size >= max) {
|
||||||
|
tabsNote.textContent = `The bottom bar holds ${max} tabs - unpin one first`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
S.pinned.add(id);
|
||||||
|
}
|
||||||
|
S.overlay = null;
|
||||||
|
tabsRender();
|
||||||
|
rerender();
|
||||||
|
});
|
||||||
|
sortable(tabsList, (ids) => {
|
||||||
|
S.order = ids;
|
||||||
|
S.overlay = null;
|
||||||
|
rerender();
|
||||||
|
});
|
||||||
|
tabsRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pointer-driven list reorder: the dragged row moves through the DOM and
|
||||||
|
the new order is read back on release. */
|
||||||
|
function sortable(list, onChange) {
|
||||||
|
list.addEventListener("pointerdown", (e) => {
|
||||||
|
const grip = e.target.closest("[data-grip]");
|
||||||
|
if (!grip) return;
|
||||||
|
const row = grip.closest("[data-id]");
|
||||||
|
e.preventDefault();
|
||||||
|
row.classList.add("is-dragging");
|
||||||
|
grip.setPointerCapture(e.pointerId);
|
||||||
|
const move = (ev) => {
|
||||||
|
const siblings = $$("[data-id]", list).filter((r) => r !== row);
|
||||||
|
const next = siblings.find((r) => {
|
||||||
|
const b = r.getBoundingClientRect();
|
||||||
|
return ev.clientY < b.top + b.height / 2;
|
||||||
|
});
|
||||||
|
if (next) list.insertBefore(row, next);
|
||||||
|
else list.appendChild(row);
|
||||||
|
};
|
||||||
|
const up = () => {
|
||||||
|
grip.removeEventListener("pointermove", move);
|
||||||
|
grip.removeEventListener("pointerup", up);
|
||||||
|
grip.removeEventListener("pointercancel", up);
|
||||||
|
row.classList.remove("is-dragging");
|
||||||
|
onChange($$("[data-id]", list).map((r) => r.dataset.id));
|
||||||
|
};
|
||||||
|
grip.addEventListener("pointermove", move);
|
||||||
|
grip.addEventListener("pointerup", up);
|
||||||
|
grip.addEventListener("pointercancel", up);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------
|
||||||
|
Accounts
|
||||||
|
--------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
const acctHost = $("#acctDemo");
|
||||||
|
if (acctHost) {
|
||||||
|
const accounts = [
|
||||||
|
{ u: "ayushya", h: "cloud.example.com", t: "#DA9AC7", l: "A" },
|
||||||
|
{ u: "ayushya.dev", h: "drive.ayushya.dev", t: "#8DCDE2", l: "D" },
|
||||||
|
{ u: "family", h: "nc.home.lan", t: "#A7D296", l: "F" },
|
||||||
|
];
|
||||||
|
let current = 0;
|
||||||
|
const draw = () => {
|
||||||
|
acctHost.innerHTML =
|
||||||
|
accounts.map((a, i) =>
|
||||||
|
`<button class="srow" data-acct="${i}">${avatar(a.l, a.t, 36, i === current)}` +
|
||||||
|
`<span class="srow-main"><b>${a.u}</b><i>${a.h}</i></span>${i === current ? `<span class="pill-tag">Active</span>` : ""}</button>`
|
||||||
|
).join("") +
|
||||||
|
`<div class="srow srow-accent">${ic("userPlus", 20)}<span class="srow-main"><b>Add account</b></span></div>`;
|
||||||
|
};
|
||||||
|
acctHost.addEventListener("click", (e) => {
|
||||||
|
const b = e.target.closest("[data-acct]");
|
||||||
|
if (!b) return;
|
||||||
|
current = Number(b.dataset.acct);
|
||||||
|
draw();
|
||||||
|
const note = $("#acctNote");
|
||||||
|
if (note) note.textContent = `Now browsing ${accounts[current].h}. Each account keeps its own sync, cache and favorites.`;
|
||||||
|
});
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------
|
||||||
|
Locks
|
||||||
|
--------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
const lockHost = $("#lockDemo");
|
||||||
|
if (lockHost) {
|
||||||
|
const L = { login: true, switching: true, hidden: false };
|
||||||
|
const rows = [
|
||||||
|
["login", "lock", "Lock with fingerprint", "Require this device's PIN or biometric to open Noo", false],
|
||||||
|
["switching", "switch", "Lock account switching", "Unlock to switch between saved accounts", true],
|
||||||
|
["hidden", "hidden", "Lock hidden files", "Unlock to reveal hidden files and folders", true],
|
||||||
|
];
|
||||||
|
const draw = () => {
|
||||||
|
lockHost.innerHTML = rows.map(([k, icon, label, sub, dep]) => {
|
||||||
|
const off = dep && !L.login;
|
||||||
|
return `<div class="srow${off ? " is-disabled" : ""}"><span class="srow-ic">${ic(icon, 20)}</span>` +
|
||||||
|
`<span class="srow-main"><b>${label}</b><i>${sub}</i></span>` +
|
||||||
|
`<button class="toggle${L[k] ? " is-on" : ""}" role="switch" aria-checked="${L[k]}" aria-label="${label}" data-lock="${k}" ${off ? "disabled" : ""}><i></i></button></div>`;
|
||||||
|
}).join("");
|
||||||
|
};
|
||||||
|
lockHost.addEventListener("click", (e) => {
|
||||||
|
const b = e.target.closest("[data-lock]");
|
||||||
|
if (!b || b.disabled) return;
|
||||||
|
L[b.dataset.lock] = !L[b.dataset.lock];
|
||||||
|
draw();
|
||||||
|
});
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------
|
||||||
|
Sync
|
||||||
|
--------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
const syncHost = $("#syncDemo");
|
||||||
|
if (syncHost) {
|
||||||
|
const Y = { all: false, notif: true, cell: false, busy: false, done: false };
|
||||||
|
const draw = () => {
|
||||||
|
const tog = (k, on) => `<button class="toggle${on ? " is-on" : ""}" role="switch" aria-checked="${on}" data-sync="${k}" aria-label="${k}"><i></i></button>`;
|
||||||
|
syncHost.innerHTML =
|
||||||
|
`<div class="srow"><span class="srow-ic">${ic("cloud", 20)}</span><span class="srow-main"><b>Sync everything</b><i>Mirror the whole account instead of picking folders</i></span>${tog("all", Y.all)}</div>` +
|
||||||
|
`<div class="srow"><span class="srow-ic">${ic("offline", 20)}</span><span class="srow-main"><b>View offline files</b><i>${Y.all ? "Every folder in this account is synced" : "3 synced folders/files"}</i></span>${ic("chevR", 18, "b-muted")}</div>` +
|
||||||
|
`<div class="srow"><span class="srow-ic">${ic("bell", 20)}</span><span class="srow-main"><b>Background sync notifications</b><i>Show a notification when a background sync updates files. Conflicts and "Sync now" always notify.</i></span>${tog("notif", Y.notif)}</div>` +
|
||||||
|
`<div class="srow"><span class="srow-ic">${ic("signal", 20)}</span><span class="srow-main"><b>Sync on cellular</b><i>Off = background sync only runs on Wi-Fi</i></span>${tog("cell", Y.cell)}</div>` +
|
||||||
|
`<div class="srow"><span class="srow-ic">${ic("refresh", 20)}</span><span class="srow-main"><b>Sync now</b>${Y.done ? `<i>Up to date - just now</i>` : ""}</span>` +
|
||||||
|
(Y.busy ? `<span class="spinner" aria-label="Syncing"></span>` : `<button class="btn-tonal" data-sync="now">Sync now</button>`) + `</div>`;
|
||||||
|
};
|
||||||
|
syncHost.addEventListener("click", (e) => {
|
||||||
|
const b = e.target.closest("[data-sync]");
|
||||||
|
if (!b) return;
|
||||||
|
const k = b.dataset.sync;
|
||||||
|
if (k === "now") {
|
||||||
|
Y.busy = true; Y.done = false; draw();
|
||||||
|
setTimeout(() => { Y.busy = false; Y.done = true; draw(); }, 1400);
|
||||||
|
} else {
|
||||||
|
Y[k] = !Y[k];
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------
|
||||||
|
Share sheet
|
||||||
|
--------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
const shareHost = $("#shareDemo");
|
||||||
|
if (shareHost) {
|
||||||
|
const people = [
|
||||||
|
{ n: "ayushya", sub: "cloud.example.com", t: "#DA9AC7", l: "A", owner: true },
|
||||||
|
{ n: "Ana Souza", sub: "ana@example.com", t: "#8DCDE2", l: "AS", perm: "Can edit" },
|
||||||
|
{ n: "Leo Park", sub: "Design team", t: "#A7D296", l: "LP", perm: "Can view" },
|
||||||
|
];
|
||||||
|
const SH = { link: true, linkPerm: "Can view", copied: false, menu: null, query: "" };
|
||||||
|
const draw = () => {
|
||||||
|
shareHost.innerHTML =
|
||||||
|
`<span class="sheet-grab"></span><div class="sheet-title"><b>Q3 Planning.pdf</b><i>2.4 MB</i></div>` +
|
||||||
|
`<section class="sh-sec"><div class="sh-head"><h5>Share with people</h5></div>` +
|
||||||
|
`<label class="field">${ic("search", 18)}<input type="text" placeholder="Name, group or email" aria-label="Search people" value="${esc(SH.query)}"></label>` +
|
||||||
|
people.map((p, i) =>
|
||||||
|
`<div class="prow">${avatar(p.l, p.t, 36)}<span class="srow-main"><b>${p.n}</b><i>${p.sub}</i></span>` +
|
||||||
|
(p.owner ? `<span class="owner">Owner</span>` : `<span class="menu-wrap"><button class="pill" data-perm="${i}">${p.perm}${ic("chevD", 14)}</button>` +
|
||||||
|
(SH.menu === i ? `<span class="mini-menu">${["Can view", "Can edit"].map((o) => `<button data-set="${i}|${o}">${o}${o === p.perm ? ic("check", 14) : ""}</button>`).join("")}<button class="danger" data-set="${i}|remove">Remove access</button></span>` : "") + `</span>`) + `</div>`
|
||||||
|
).join("") + `</section>` +
|
||||||
|
`<section class="sh-sec"><div class="sh-head"><h5>Share link</h5><button class="toggle${SH.link ? " is-on" : ""}" role="switch" aria-checked="${SH.link}" data-link aria-label="Share link"><i></i></button></div>` +
|
||||||
|
(SH.link
|
||||||
|
? `<div class="prow"><span class="srow-ic">${ic("link", 20)}</span><span class="srow-main"><b>Public link</b><i>cloud.example.com/s/q3-plan</i></span><button class="pill" data-linkperm>${SH.linkPerm}${ic("chevD", 14)}</button></div>` +
|
||||||
|
`<button class="btn-primary-sm" data-copy>${ic("copy", 16)}${SH.copied ? "Copied" : "Copy link"}</button>`
|
||||||
|
: `<p class="sh-cap">Turn on to create a public link.</p>`) + `</section>` +
|
||||||
|
`<section class="sh-sec"><div class="sh-head"><h5>Send file directly</h5></div><button class="btn-tonal btn-wide" data-send>${ic("send", 16)}Send via another app</button>` +
|
||||||
|
`<p class="sh-cap">Link settings above don't apply - this sends the file's bytes directly.</p></section>`;
|
||||||
|
};
|
||||||
|
shareHost.addEventListener("click", (e) => {
|
||||||
|
const t = e.target;
|
||||||
|
let b;
|
||||||
|
if ((b = t.closest("[data-perm]"))) {
|
||||||
|
const i = Number(b.dataset.perm);
|
||||||
|
SH.menu = SH.menu === i ? null : i;
|
||||||
|
} else if ((b = t.closest("[data-set]"))) {
|
||||||
|
const [i, v] = b.dataset.set.split("|");
|
||||||
|
if (v === "remove") people.splice(Number(i), 1);
|
||||||
|
else people[Number(i)].perm = v;
|
||||||
|
SH.menu = null;
|
||||||
|
} else if (t.closest("[data-link]")) {
|
||||||
|
SH.link = !SH.link;
|
||||||
|
SH.copied = false;
|
||||||
|
} else if (t.closest("[data-linkperm]")) {
|
||||||
|
SH.linkPerm = SH.linkPerm === "Can view" ? "Can edit" : "Can view";
|
||||||
|
} else if (t.closest("[data-copy]")) {
|
||||||
|
SH.copied = true;
|
||||||
|
setTimeout(() => { SH.copied = false; draw(); }, 1600);
|
||||||
|
} else if (!t.closest(".mini-menu")) {
|
||||||
|
if (SH.menu === null) return;
|
||||||
|
SH.menu = null;
|
||||||
|
} else return;
|
||||||
|
draw();
|
||||||
|
});
|
||||||
|
shareHost.addEventListener("input", (e) => { SH.query = e.target.value; });
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------
|
||||||
|
Action bar
|
||||||
|
--------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
const ACTIONS = {
|
||||||
|
favorite: ["Favorite", "star"], share: ["Share", "share"], download: ["Download", "download"],
|
||||||
|
delete: ["Delete", "trash"], copy: ["Copy", "copy"], move: ["Move", "move"],
|
||||||
|
rename: ["Rename", "rename"], sync: ["Sync to device", "offline"], details: ["Details", "info"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const actionHost = $("#actionDemo");
|
||||||
|
if (actionHost) {
|
||||||
|
let order = Object.keys(ACTIONS);
|
||||||
|
let more = false;
|
||||||
|
const INLINE = 3;
|
||||||
|
const orderList = $("#actionOrder");
|
||||||
|
const bar = $("#actionBar");
|
||||||
|
|
||||||
|
const drawBar = () => {
|
||||||
|
const inline = order.slice(0, INLINE);
|
||||||
|
const rest = order.slice(INLINE);
|
||||||
|
bar.innerHTML =
|
||||||
|
`<div class="selbar"><span class="selbar-x">${ic("x", 18)}</span><b>2 selected</b>` +
|
||||||
|
`<span class="selbar-acts">${inline.map((k) => `<button class="selbar-btn" aria-label="${ACTIONS[k][0]}" title="${ACTIONS[k][0]}">${ic(ACTIONS[k][1], 20)}</button>`).join("")}` +
|
||||||
|
`<button class="selbar-btn" data-more aria-label="More" aria-expanded="${more}">${ic("moreH", 20)}</button></span></div>` +
|
||||||
|
(more ? `<div class="sheet-mini" role="menu"><span class="sheet-grab"></span>${rest.map((k) => `<div class="srow">${`<span class="srow-ic">${ic(ACTIONS[k][1], 20)}</span>`}<span class="srow-main"><b>${ACTIONS[k][0]}</b></span></div>`).join("")}</div>` : "");
|
||||||
|
};
|
||||||
|
const drawList = () => {
|
||||||
|
orderList.innerHTML = order.map((k, i) =>
|
||||||
|
`<li class="tab-row" data-id="${k}"><span class="grip" data-grip aria-label="Drag to reorder">${ic("grip", 18)}</span>` +
|
||||||
|
`<span class="tab-ic">${ic(ACTIONS[k][1], 20)}</span><span class="tab-label">${ACTIONS[k][0]}</span>` +
|
||||||
|
`<span class="where ${i < INLINE ? "is-bar" : ""}">${i < INLINE ? "In bar" : "More"}</span></li>`
|
||||||
|
).join("");
|
||||||
|
};
|
||||||
|
sortable(orderList, (ids) => { order = ids; drawList(); drawBar(); });
|
||||||
|
bar.addEventListener("click", (e) => {
|
||||||
|
if (e.target.closest("[data-more]")) { more = !more; drawBar(); }
|
||||||
|
});
|
||||||
|
drawList();
|
||||||
|
drawBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------
|
||||||
|
Swiping
|
||||||
|
--------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
const swipeHost = $("#swipeDemo");
|
||||||
|
if (swipeHost) {
|
||||||
|
const BLOCK = 96, THROUGH = 1.8;
|
||||||
|
const cfg = { right: "favorite", left: "delete" };
|
||||||
|
const files = [ROWS.files[3], ROWS.files[4], ROWS.files[6], ROWS.files[7]].map((r) => ({ r, fav: r[3] && r[3].includes("fav") }));
|
||||||
|
const toast = $("#swipeToast");
|
||||||
|
let toastTimer;
|
||||||
|
const say = (msg) => {
|
||||||
|
toast.textContent = msg;
|
||||||
|
toast.classList.add("is-on");
|
||||||
|
clearTimeout(toastTimer);
|
||||||
|
toastTimer = setTimeout(() => toast.classList.remove("is-on"), 1800);
|
||||||
|
};
|
||||||
|
const meta = {
|
||||||
|
none: null,
|
||||||
|
favorite: ["Favorite", "star", "ac"],
|
||||||
|
delete: ["Delete", "trash", "dgf"],
|
||||||
|
share: ["Share", "share", "ac"],
|
||||||
|
};
|
||||||
|
const list = $("#swipeList");
|
||||||
|
|
||||||
|
const drawRows = () => {
|
||||||
|
list.innerHTML = files.map((f, i) => {
|
||||||
|
const row = fileRow([f.r[0], f.r[1], f.r[2], (f.fav ? "fav " : "") + (f.r[3] || "").replace("fav", "")]);
|
||||||
|
const blk = (side) => {
|
||||||
|
const m = meta[cfg[side]];
|
||||||
|
if (!m) return "";
|
||||||
|
return `<div class="sw-block sw-${side}" style="background:var(--${m[2]})" data-act-side="${side}"><span class="sw-inner">${ic(m[1], 20)}<b>${f.fav && cfg[side] === "favorite" ? "Unfavorite" : m[0]}</b></span></div>`;
|
||||||
|
};
|
||||||
|
return `<div class="sw" data-i="${i}">${blk("right")}${blk("left")}<div class="sw-fg">${row}</div></div>`;
|
||||||
|
}).join("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const trigger = (i, side) => {
|
||||||
|
const a = cfg[side];
|
||||||
|
const f = files[i];
|
||||||
|
if (a === "favorite") { f.fav = !f.fav; say(f.fav ? `Added ${f.r[0]} to favorites` : `Removed ${f.r[0]} from favorites`); drawRows(); }
|
||||||
|
else if (a === "delete") {
|
||||||
|
const el = $(`.sw[data-i="${i}"]`, list);
|
||||||
|
el.classList.add("is-gone");
|
||||||
|
say(`Moved ${f.r[0]} to trash (demo - it comes back)`);
|
||||||
|
setTimeout(drawRows, 1500);
|
||||||
|
} else if (a === "share") { say(`Share sheet opens for ${f.r[0]}`); drawRows(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
let drag = null;
|
||||||
|
list.addEventListener("pointerdown", (e) => {
|
||||||
|
const sw = e.target.closest(".sw");
|
||||||
|
if (!sw) return;
|
||||||
|
const blk = e.target.closest("[data-act-side]");
|
||||||
|
if (blk && sw.dataset.open === blk.dataset.actSide) { trigger(Number(sw.dataset.i), blk.dataset.actSide); return; }
|
||||||
|
const fg = $(".sw-fg", sw);
|
||||||
|
const base = Number(sw.dataset.dx || 0);
|
||||||
|
drag = { sw, fg, x0: e.clientX, base, moved: false, id: e.pointerId };
|
||||||
|
sw.setPointerCapture?.(e.pointerId);
|
||||||
|
});
|
||||||
|
list.addEventListener("pointermove", (e) => {
|
||||||
|
if (!drag || e.pointerId !== drag.id) return;
|
||||||
|
let dx = drag.base + (e.clientX - drag.x0);
|
||||||
|
if (Math.abs(e.clientX - drag.x0) > 4) drag.moved = true;
|
||||||
|
const maxRight = cfg.right === "none" ? 0 : BLOCK * 2.2;
|
||||||
|
const maxLeft = cfg.left === "none" ? 0 : BLOCK * 2.2;
|
||||||
|
dx = Math.max(-maxLeft, Math.min(maxRight, dx));
|
||||||
|
drag.dx = dx;
|
||||||
|
drag.sw.dataset.dx = dx;
|
||||||
|
drag.sw.classList.add("is-dragging");
|
||||||
|
drag.sw.classList.toggle("show-right", dx > 0);
|
||||||
|
drag.sw.classList.toggle("show-left", dx < 0);
|
||||||
|
drag.sw.style.setProperty("--dx", dx + "px");
|
||||||
|
drag.sw.classList.toggle("is-through", Math.abs(dx) >= BLOCK * THROUGH);
|
||||||
|
});
|
||||||
|
const end = (e) => {
|
||||||
|
if (!drag || e.pointerId !== drag.id) return;
|
||||||
|
const { sw } = drag;
|
||||||
|
const dx = drag.dx ?? drag.base;
|
||||||
|
const side = dx > 0 ? "right" : "left";
|
||||||
|
sw.classList.remove("is-dragging", "is-through");
|
||||||
|
let target = 0;
|
||||||
|
if (Math.abs(dx) >= BLOCK * THROUGH) {
|
||||||
|
trigger(Number(sw.dataset.i), side);
|
||||||
|
drag = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Math.abs(dx) > BLOCK / 2) target = Math.sign(dx) * BLOCK;
|
||||||
|
sw.dataset.dx = target;
|
||||||
|
sw.dataset.open = target === 0 ? "" : side;
|
||||||
|
sw.style.setProperty("--dx", target + "px");
|
||||||
|
if (target === 0) sw.classList.remove("show-right", "show-left");
|
||||||
|
drag = null;
|
||||||
|
};
|
||||||
|
list.addEventListener("pointerup", end);
|
||||||
|
list.addEventListener("pointercancel", end);
|
||||||
|
|
||||||
|
// Pickers
|
||||||
|
$$("[data-swipe]", swipeHost).forEach((group) => {
|
||||||
|
const side = group.dataset.swipe;
|
||||||
|
const sync = () => $$("[data-val]", group).forEach((b) => b.classList.toggle("is-active", b.dataset.val === cfg[side]));
|
||||||
|
group.addEventListener("click", (e) => {
|
||||||
|
const b = e.target.closest("[data-val]");
|
||||||
|
if (!b) return;
|
||||||
|
cfg[side] = b.dataset.val;
|
||||||
|
sync();
|
||||||
|
drawRows();
|
||||||
|
});
|
||||||
|
sync();
|
||||||
|
});
|
||||||
|
drawRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------
|
||||||
|
Page chrome: mobile nav, hero parallax, scroll reveal
|
||||||
|
--------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
const navToggle = $("#navToggle");
|
||||||
|
const mobileNav = $("#mobileNav");
|
||||||
|
if (navToggle && mobileNav) {
|
||||||
|
navToggle.addEventListener("click", () => {
|
||||||
|
const open = mobileNav.classList.toggle("is-open");
|
||||||
|
navToggle.setAttribute("aria-expanded", String(open));
|
||||||
|
navToggle.setAttribute("aria-label", open ? "Close menu" : "Open menu");
|
||||||
|
});
|
||||||
|
$$("a", mobileNav).forEach((a) =>
|
||||||
|
a.addEventListener("click", () => {
|
||||||
|
mobileNav.classList.remove("is-open");
|
||||||
|
navToggle.setAttribute("aria-expanded", "false");
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const heroVisual = $("#heroVisual");
|
||||||
|
if (heroVisual && !reduceMotion) {
|
||||||
|
const layers = $$("[data-depth]", heroVisual).map((el) => ({ el, depth: parseFloat(el.dataset.depth) || 0.3 }));
|
||||||
|
let scrollOffset = 0;
|
||||||
|
let pointer = { x: 0, y: 0 };
|
||||||
|
let ticking = false;
|
||||||
|
|
||||||
|
const apply = () => {
|
||||||
|
const rect = heroVisual.getBoundingClientRect();
|
||||||
|
if (rect.bottom > 0 && rect.top < window.innerHeight) {
|
||||||
|
for (const l of layers) {
|
||||||
|
l.el.style.transform = `translate3d(${pointer.x * l.depth * 14}px, ${scrollOffset * l.depth * 40 + pointer.y * l.depth * 14}px, 0)`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ticking = false;
|
||||||
|
};
|
||||||
|
const tick = () => { if (!ticking) { ticking = true; requestAnimationFrame(apply); } };
|
||||||
|
const onScroll = () => {
|
||||||
|
const rect = heroVisual.getBoundingClientRect();
|
||||||
|
scrollOffset = (window.innerHeight / 2 - (rect.top + rect.height / 2)) / window.innerHeight;
|
||||||
|
tick();
|
||||||
|
};
|
||||||
|
window.addEventListener("scroll", onScroll, { passive: true });
|
||||||
|
heroVisual.addEventListener("pointermove", (e) => {
|
||||||
|
const rect = heroVisual.getBoundingClientRect();
|
||||||
|
pointer.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
||||||
|
pointer.y = ((e.clientY - rect.top) / rect.height) * 2 - 1;
|
||||||
|
tick();
|
||||||
|
});
|
||||||
|
heroVisual.addEventListener("pointerleave", () => { pointer = { x: 0, y: 0 }; tick(); });
|
||||||
|
onScroll();
|
||||||
|
}
|
||||||
|
|
||||||
|
const revealTargets = $$(".card, .showcase-copy, .showcase-demo, .tab-chip, .privacy-points li");
|
||||||
|
revealTargets.forEach((el) => el.classList.add("reveal"));
|
||||||
|
if ("IntersectionObserver" in window && !reduceMotion) {
|
||||||
|
const io = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
entry.target.classList.add("is-visible");
|
||||||
|
io.unobserve(entry.target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.12, rootMargin: "0px 0px -40px 0px" }
|
||||||
|
);
|
||||||
|
revealTargets.forEach((el) => io.observe(el));
|
||||||
|
} else {
|
||||||
|
revealTargets.forEach((el) => el.classList.add("is-visible"));
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|||||||
+685
-411
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user