Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@
 · 
<a href="https://filekit.mintlify.app/roadmap"><b>Roadmap to 1.0</b></a>
</p>

<a href="https://kotlinfoundation.org/news/grants-program-winners-26/">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="brand/exports/grant-banner-dark.webp" />
<img src="brand/exports/grant-banner-light.webp" width="960" alt="FileKit is supported by the Kotlin Foundation Grants Program. Read the announcement." />
</picture>
</a>
</div>

## Installation
Expand Down
7 changes: 5 additions & 2 deletions brand/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ npm run render -- sample # only the sample app icons and logos
npm run preview # serve the sources to tweak them live in a browser
```

`npm run preview` prints two URLs: the brand board (every export and the palette on one page) and the live
hero (`src/hero.html`). The hero fills the browser window, so resizing the window previews other formats.
`npm run preview` prints three URLs: the brand board (every export and the palette on one page), the live
hero (`src/hero.html`) and the grant banner (`src/banner.html`, configured under `banner` in `palette.json`).
The hero fills the browser window, so resizing the window previews other formats.
Add `?seed=3.6` to the hero URL to try another background composition without editing any file.

Rendering runs locally only; CI does not build these assets.
Expand All @@ -28,6 +29,8 @@ Rendering runs locally only; CI does not build these assets.
| README hero | `brand/exports/readme-hero.webp` | 1920×960, rounded corners |
| GitHub social preview | `brand/exports/social-preview.jpg` | 1280×640, upload in the repo settings |
| Docs hero | `docs/brand/hero.webp` | 1920×1080, rounded corners |
| README grant banner | `brand/exports/grant-banner-{dark,light}.webp` | 1920×240, rounded corners, with the call to action |
| Docs grant banner | `docs/brand/grant-banner-{dark,light}.webp` | 1920×240 |
| Open Graph image | `docs/brand/og.jpg` | 1200×630 |
| Logo for light backgrounds | `docs/brand/logo-light.svg` | mark + wordmark, outlined |
| Logo for dark backgrounds | `docs/brand/logo-dark.svg` | mark + wordmark, outlined |
Expand Down
Binary file added brand/exports/grant-banner-dark.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added brand/exports/grant-banner-light.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions brand/palette.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,39 @@
"grain": 0,
"focus": 0.5
}
},
"banner": {
"seed": 9.1,
"pieces": 90,
"background": {
"dark": {
"grain": 0.06,
"focus": 0.45
},
"light": {
"grain": 0.05,
"focus": -0.5
}
},
"text": {
"dark": "#FFF1D0",
"light": "#0F5A36"
},
"confetti": {
"dark": [
"#F2D37A",
"#FFF1D0",
"#FFFFFF",
"#3DBA6E",
"#E3C266"
],
"light": [
"#0F5A36",
"#2E9A5C",
"#E7C25E",
"#FFFFFF",
"#D8B24C"
]
}
}
}
14 changes: 11 additions & 3 deletions brand/render.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// npm run render -- sample # only the sample app icons
// npm run preview # serve the sources to tweak them live in a browser
//
// Raster images are screenshots of src/hero.html taken by headless Chromium (Playwright).
// Raster images are screenshots of src/hero.html and src/banner.html taken by headless Chromium (Playwright).
// SVG logos are generated from src/mark.js and the outlines of Inter Bold Italic (opentype.js).
// App icons are screenshots of src/icon.html; .icns and .ico files are assembled from those PNGs.

Expand All @@ -30,6 +30,12 @@ const HEROES = [
{ file: 'docs/brand/hero.webp', width: 1920, height: 1080, theme: 'dark', rounded: true },
{ file: 'docs/brand/og.jpg', width: 1200, height: 630, theme: 'dark' },
];

// Grant banner (src/banner.html), in both themes. The README one is a link, so it carries its call to action.
const BANNERS = ['dark', 'light'].flatMap((theme) => [
{ file: `brand/exports/grant-banner-${theme}.webp`, width: 1920, height: 240, theme, rounded: true, cta: true },
{ file: `docs/brand/grant-banner-${theme}.webp`, width: 1920, height: 240, theme },
]);
const QUALITY = 0.92;

// ---------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -135,6 +141,7 @@ if (process.argv[2] === '--preview') {
const { base } = await serve(4173);
console.log(`Brand board: ${base}/brand/src/board.html`);
console.log(`Live hero: ${base}/brand/src/hero.html (resize the window to try formats, add ?seed=3.6 to try a composition)`);
console.log(`Grant banner: ${base}/brand/src/banner.html?cta=1 (add &theme=light or &seed=3.6)`);
console.log('Ctrl+C to stop.');
} else {
await render(process.argv[2]);
Expand All @@ -149,7 +156,7 @@ async function render(filter) {
console.log('wrote', svg.file);
}

const heroes = HEROES.filter((h) => wanted(h.file));
const heroes = [...HEROES, ...BANNERS].filter((h) => wanted(h.file));
const icons = ICONS.filter((i) => wanted(i.file));
if (heroes.length === 0 && icons.length === 0) return;
const { server, base } = await serve();
Expand All @@ -158,7 +165,8 @@ async function render(filter) {
for (const hero of heroes) {
const page = await browser.newPage({ viewport: { width: hero.width, height: hero.height } });
page.on('pageerror', (e) => console.error(hero.file, e.message));
await page.goto(`${base}/brand/src/hero.html?theme=${hero.theme}&rounded=${hero.rounded ? 1 : 0}`);
const source = BANNERS.includes(hero) ? 'banner' : 'hero';
await page.goto(`${base}/brand/src/${source}.html?theme=${hero.theme}&rounded=${hero.rounded ? 1 : 0}&cta=${hero.cta ? 1 : 0}`);
await page.waitForSelector('body[data-ready]');
await mkdir(dirname(join(REPO, hero.file)), { recursive: true });
if (hero.file.endsWith('.jpg')) {
Expand Down
98 changes: 98 additions & 0 deletions brand/src/banner.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<!doctype html>
<!--
FileKit grant banner: a thin strip on the silk background with a sprinkle of confetti.
Fills the viewport, so the export script picks the size.
URL parameters: theme=dark|light, rounded=1 (transparent rounded corners),
cta=1 (adds the "Read the announcement" line, for the README where the whole image is a link),
seed=<number> (try another background and confetti layout without editing palette.json).
All sizes are in cqw (percent of the image width) so every format keeps the same proportions.
-->
<html>
<head>
<meta charset="utf-8">
<style>
@font-face { font-family: 'Inter'; font-weight: 400; src: url('../fonts/Inter-Regular.woff2') format('woff2'); }
@font-face { font-family: 'Inter'; font-weight: 500; src: url('../fonts/Inter-Medium.woff2') format('woff2'); }
@font-face { font-family: 'Inter'; font-weight: 700; font-style: italic; src: url('../fonts/Inter-BoldItalic.ttf') format('truetype'); }

* { box-sizing: border-box; margin: 0; }
html, body { width: 100%; height: 100%; background: transparent; overflow: hidden; }
.frame { position: absolute; inset: 0; container-type: size; overflow: hidden; }
.rounded .frame { border-radius: 1.6cqw; }
canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.confetti { position: absolute; inset: 0; }
.confetti i { position: absolute; display: block; border-radius: .08cqw; }
.text { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 2cqw; color: var(--text); text-align: center; white-space: nowrap; }
.line { font: 400 2.7cqw/1 'Inter'; text-box: trim-both cap alphabetic; }
.line b { font: italic 700 2.7cqw/1 'Inter'; letter-spacing: -0.01em; }
.cta { font: 500 1.6cqw/1 'Inter'; opacity: .85; text-decoration: underline; text-underline-offset: .3cqw;
text-decoration-thickness: .1cqw; text-box: trim-both cap alphabetic; }
</style>
</head>
<body>
<div class="frame">
<canvas></canvas>
<div class="confetti"></div>
<div class="text">
<div class="line">Supported by the <b>Kotlin Foundation Grants Program</b></div>
<div class="cta" hidden>Read the announcement →</div>
</div>
</div>
<script type="module">
import { paintBackground } from './background.js';

const params = new URLSearchParams(location.search);
const theme = params.get('theme') ?? 'dark';
if (params.get('rounded') === '1') document.body.classList.add('rounded');
if (params.get('cta') === '1') document.querySelector('.cta').hidden = false;

const palette = await (await fetch('../palette.json')).json();
const look = { ...palette.background[theme], ...palette.banner.background[theme] };
const seed = params.has('seed') ? Number(params.get('seed')) : palette.banner.seed;
const frame = document.querySelector('.frame');
frame.style.setProperty('--text', palette.banner.text[theme]);
await document.fonts.ready;

// Small deterministic PRNG so a seed always gives the same confetti.
function random(s) {
let a = Math.floor(s * 1e6) >>> 0;
return () => {
a = (a + 0x6D2B79F5) >>> 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}

function draw() {
paintBackground(document.querySelector('canvas'), { ...look, seed });

// Confetti thins out towards the center so the text stays clean.
const rand = random(seed);
const colors = palette.banner.confetti[theme];
const W = frame.clientWidth, H = frame.clientHeight, cqw = W / 100;
const pieces = [];
while (pieces.length < palette.banner.pieces) {
const x = rand(), y = rand();
const dx = (x - 0.5) / 0.34, dy = (y - 0.5) / 0.62;
if (dx * dx + dy * dy < 1 + 0.6 * rand()) continue;
const round = rand() < 0.3;
const w = round ? 0.45 + 0.25 * rand() : 0.8 + 0.7 * rand();
const h = round ? w : 0.26 + 0.14 * rand();
pieces.push(`<i style="left:${(x * W).toFixed(1)}px;top:${(y * H).toFixed(1)}px;width:${(w * cqw).toFixed(1)}px;` +
`height:${(h * cqw).toFixed(1)}px;background:${colors[Math.floor(rand() * colors.length)]};` +
`opacity:${(0.65 + 0.35 * rand()).toFixed(2)};${round ? 'border-radius:50%;' : ''}` +
`transform:translate(-50%,-50%) rotate(${Math.floor(rand() * 180)}deg)"></i>`);
}
document.querySelector('.confetti').innerHTML = pieces.join('');
}

draw();
// Keep the live preview (npm run preview) accurate when the window is resized.
let pending = 0;
addEventListener('resize', () => { cancelAnimationFrame(pending); pending = requestAnimationFrame(draw); });
document.body.dataset.ready = '1';
</script>
</body>
</html>
2 changes: 2 additions & 0 deletions brand/src/board.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ <h2>Social preview · brand/exports/social-preview.jpg — OG image · docs/bran
<div class="two"><img src="../exports/social-preview.jpg" alt=""><img src="../../docs/brand/og.jpg" alt=""></div>
<h2>Docs hero · docs/brand/hero.webp</h2>
<img src="../../docs/brand/hero.webp" alt="">
<h2>Grant banner · brand/exports/grant-banner-{dark,light}.webp</h2>
<div class="two"><img src="../exports/grant-banner-dark.webp" alt=""><img src="../exports/grant-banner-light.webp" alt=""></div>
<h2>Logos · docs/brand/logo-{light,dark}.svg — favicon · docs/brand/favicon.svg</h2>
<div class="two">
<div class="tile" style="background:#fff"><img src="../../docs/brand/logo-light.svg" style="height:40px" alt=""><img src="../../docs/brand/favicon.svg" style="height:32px" alt=""><img src="../../docs/brand/favicon.svg" style="height:16px" alt=""></div>
Expand Down
Binary file added docs/brand/grant-banner-dark.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/brand/grant-banner-light.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
"dark": "#1A7F4B"
},
"favicon": "/brand/favicon.svg",
"banner": {
"content": "🎉 FileKit is supported by the Kotlin Foundation Grants Program. [Read the announcement](https://kotlinfoundation.org/news/grants-program-winners-26/)",
"dismissible": true
},
"navigation": {
"groups": [
{
Expand Down
3 changes: 3 additions & 0 deletions docs/introduction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ description: Pick, save, read, and write files with one Kotlin Multiplatform API
og:title: FileKit
---

import { GrantBanner } from '/snippets/grant-banner.jsx'
import { QuoteGrid, QuoteCard } from '/snippets/quote-card.jsx'
import { UsedBy } from '/snippets/used-by.jsx'

Expand All @@ -19,6 +20,8 @@ import { UsedBy } from '/snippets/used-by.jsx'

**FileKit is the file layer for Kotlin Multiplatform.** Open native file, photo, directory, and camera pickers, save and share files, and read or write them through a single `PlatformFile` type. The same code runs on Android, iOS, macOS, desktop JVM (Windows, macOS, Linux), Linux and Windows native, JS, and Wasm, and FileKit Core also runs on watchOS.

<GrantBanner />

## FileKit in 30 seconds

Let the user pick a photo, then show it or read its bytes.
Expand Down
4 changes: 4 additions & 0 deletions docs/roadmap.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ title: 'Roadmap to 1.0'
description: 'Where FileKit is heading, what 1.0 will guarantee, and how you can help'
---

import { grantPostUrl } from '/snippets/grant-banner.jsx'

FileKit is already used in production on Android, iOS, desktop and the web. The goal of 1.0 is not a big list of new features. It is a **promise**: a reviewed public API, clear compatibility guarantees, predictable behavior across platforms, and documentation that tells you exactly what to expect.

FileKit is supported by the <a href={grantPostUrl}>Kotlin Foundation Grants Program</a>. The grant supports the work toward 1.0 described on this page.

<Info>
**Target: FileKit 1.0 in Q1 2027.** This is a best-effort target, not a fixed date. The order of the steps below matters more than the calendar, and this page is updated as work lands.
</Info>
Expand Down
29 changes: 29 additions & 0 deletions docs/snippets/grant-banner.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Grant banner for the docs: the brand strip, then a short line linking to the announcement.
* The images follow the docs light/dark theme (rendered by brand/render.mjs).
*
* Usage:
* import { GrantBanner, grantPostUrl } from "/snippets/grant-banner.jsx"
*
* <GrantBanner />
*/
export const grantPostUrl = "https://kotlinfoundation.org/news/grants-program-winners-26/"

export const GrantBanner = () => {
const alt = "FileKit is supported by the Kotlin Foundation Grants Program"
return (
<a
href={grantPostUrl}
className="not-prose group my-6 block overflow-hidden rounded-2xl border border-gray-950/10 dark:border-white/10"
>
<img src="/brand/grant-banner-light.webp" alt={alt} width="1920" height="240" className="block w-full dark:hidden" />
<img src="/brand/grant-banner-dark.webp" alt={alt} width="1920" height="240" className="hidden w-full dark:block" />
<p className="px-5 py-3 text-sm text-gray-700 dark:text-gray-300">
The grant supports the work toward FileKit 1.0.{" "}
<span className="whitespace-nowrap font-semibold text-primary group-hover:underline dark:text-primary-light">
Read the announcement →
</span>
</p>
</a>
)
}
Loading