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
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.20.0",
"globals": "^17.7.0",
"lapikit": "^0.0.0-insiders.a294d65",
"lapikit": "^0.0.0-insiders.5296bfc",
"mdsvex": "^0.12.7",
"prettier": "^3.9.3",
"prettier-plugin-svelte": "^3.5.2",
Expand Down
14 changes: 14 additions & 0 deletions src/content/docs/components/field.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,20 @@ The `children` snippet receives the attributes to spread on the control:
</kit:field>
```

In TypeScript, type the parameter with `FieldControlProps`:

```svelte
<script lang="ts">
import type { FieldControlProps } from 'lapikit/components';
</script>

<kit:field message="Help">
{#snippet children(control: FieldControlProps)}
<input {...control} bind:value />
{/snippet}
</kit:field>
```

The field does not know the value of the control: pass `dirty` to show the clear button, `onclear` to empty the value, and `counter` for the text at the end of the message line.

### Without preprocess Lapikit
Expand Down
8 changes: 4 additions & 4 deletions src/content/docs/components/modal.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ layout: 'doc_page'
import persistentCode from '$examples/components/modal/persistent.svelte?raw';
</script>

The `kit:modal` component displays an overlay panel above the main content. It is designed for dialogs, contextual messages, forms or alerts requiring direct interaction.
The `kit:modal` component displays a floating window above the main content, without blocking the page: the user can still click, scroll and type behind it. It is designed for chat panels, tool panels, notices or any content that lives next to the page.

The modal can be centered, fixed at the top or bottom of the screen, and closes in different ways depending on the options activated (outside click, Escape key, etc.). It is a flexible alternative to [`kit:dialog`](/docs/components/dialog) when you need more control over the structure, behavior or styling of the modal content, or when you want to manage the backdrop at the application level.
The modal can be centered or fixed to a corner or an edge of the screen, and closes with the Escape key (the top modal first). When the page must wait for an answer, use [`kit:dialog`](/docs/components/dialog) instead: it blocks the page, traps the focus and has a backdrop.

<LazyRepl title="modal.svelte" presentation lang="svelte" content={previewCode}>
<Preview/>
</LazyRepl>

`kit:modal` is a div-based overlay positioned fixed over the viewport. Unlike `kit:dialog`, it does not use the native `<dialog>` element and has no built-in backdrop a global backdrop is typically managed at the application level. Use `contain` to embed the modal relative to its parent instead.
`kit:modal` renders a native `<dialog>` opened with `show()`: it is announced as a dialog, takes the focus when it opens and gives it back when it closes, but it does not lock the scroll and has no backdrop. Use `contain` to embed the modal in its parent, with an overlay over that parent only.

> `open` is bindable set it to `true` to open, `false` to close, or bind it to react to closes.

Expand Down Expand Up @@ -77,7 +77,7 @@ If you aren't using the Lapikit preprocessor in `svelte.config.js`, import the c

| props | type | description | default |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ----------- |
| ref $bindable | `HTMLElement` | Bindable reference to the rendered DOM element. | `null` |
| ref $bindable | `HTMLDialogElement` | Bindable reference to the `<dialog>` element. | `null` |
| class | `string \| array \| object` | Classes of the modal, with the rules of Svelte (clsx). | |
| style | `string` | Inline style of the modal. | |
| s-class | `string \| array \| object` | Classes of the modal, merged with `class`. | |
Expand Down
2 changes: 1 addition & 1 deletion src/content/docs/components/tooltip.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ It is particularly useful for enhancing the user experience by explaining icons,
<Preview/>
</LazyRepl>

`kit:tooltip` wraps its `children` in a `<span>` that listens for `mouseenter`, `mouseleave`, `focusin`, and `focusout`. The tooltip appears after `delayDuration` milliseconds (default 850ms) and is positioned fixed via a separate element. Set `label` for a plain text tooltip, or use the `tooltip` snippet for rich content.
`kit:tooltip` wraps its `children` in a `<span>` that listens for `mouseenter`, `mouseleave`, `focusin`, and `focusout`. The tooltip appears after `delayDuration` milliseconds on hover (default 850ms), right away on focus, and is shown in the top layer next to its trigger. The pointer can move onto the tooltip without closing it, and Escape closes it, as required by [WCAG 1.4.13](https://www.w3.org/WAI/WCAG22/Understanding/content-on-hover-or-focus.html). Set `label` for a plain text tooltip, or use the `tooltip` snippet for rich content.

## Examples of tooltip

Expand Down
2 changes: 1 addition & 1 deletion src/content/docs/customize.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Our recommendation is to base your approach on how you manage themes in your exi

### Theme mode support

Lapikit resolves its color tokens through the data-kit-theme attribute. Set it to `light` or `dark` on the element you want to theme usually `<html>`, but any container works. Because it's a plain attribute, you keep full control: toggle it from your own logic, persist the choice, or scope a dark section inside an otherwise light page with function [`useTheme()`](/docs/hooks/use-theme).
Lapikit resolves its color tokens through the data-kit-theme attribute. Set it to `light`, `dark` or `system` (follows `prefers-color-scheme`) on the element you want to theme usually `<html>`, but any container works. Because it's a plain attribute, you keep full control: toggle it from your own logic, persist the choice, or scope a dark section inside an otherwise light page with function [`useTheme()`](/docs/hooks/use-theme). With SSR, [`themeScript()`](/docs/hooks/use-theme#avoid-the-flash-of-the-wrong-theme) applies the stored theme before the first paint.

<KitThemeColor/>

Expand Down
20 changes: 19 additions & 1 deletion src/content/docs/environments.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,30 @@ Then add it to your `eslint.config.js`:

<LazyRepl title="eslint.config.js" lang="js" content={eslintConfigLapikitCode} />

This config handles the `@typescript-eslint/no-unused-vars` rule for lapikit-specific snippets such as `activator`, `indicator`, `append`, `prepend`, `close`, `load`, and `tooltip`.
This config handles the `@typescript-eslint/no-unused-vars` rule for lapikit-specific snippets such as `activator`, `indicator`, `append`, `prepend`, `close`, `load`, `tooltip` and `children` (with a parameter, as in `kit:field`).

> **Note:** `eslint-config-lapikit` configures the `@typescript-eslint/no-unused-vars` rule. If you already have a custom configuration for this rule, place the lapikit config first so your own rules take precedence:

<LazyRepl title="eslint.config.js" lang="js" content={eslintConfigNoUnusedVarsCode} />

## Use the types of the components

`lapikit/components` exports the props type of every component (`ButtonProps`, `CardProps`, `TextfieldProps`...), the types of the snippet parameters (`FieldControlProps`, `ModelDropdownProps`...) and the shared types (`ColorProps`, `ElevationProps`, `RoundedType`, `DensityType`...). Use them to type a snippet parameter or to build your own component on top of a Lapikit one:

```svelte
<script lang="ts">
import type { ButtonProps } from 'lapikit/components';

let { children, ...rest }: ButtonProps = $props();
</script>

<kit:btn variant="tonal" {...rest}>
{@render children?.()}
</kit:btn>
```

The labs components export theirs from `lapikit/labs/components` (`SheetProps`).

## Known TypeScript and editor limitations

### Snippet warnings in TypeScript
Expand Down
3 changes: 2 additions & 1 deletion src/content/docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ layout: 'doc_page'
Before installing Lapikit, make sure you have:

- basic familiarity with the command line
- an existing Svelte project (SvelteKit, Vite, or another Svelte setup)
- an existing Svelte project (SvelteKit, Vite, or another Svelte setup) with Svelte 5.20 or later
- Node 20.19 or later (Node 22.12 or later on the 22 branch), as required by Vite 7

SvelteKit is recommended for the best experience, but Lapikit works with any Svelte project.

Expand Down
43 changes: 32 additions & 11 deletions src/content/docs/hooks/use-theme.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,36 +17,57 @@ layout: 'doc_page'

## Discover the useTheme action in Lapikit

The `useTheme` function allows you to dynamically change your application's theme by modifying the CSS class applied to the root HTML element. This makes it easy to implement dark or light themes, thereby improving the user experience.
The `useTheme` function changes the theme of your application: it sets the `data-kit-theme` attribute on the `<html>` element and keeps the choice in `localStorage`. Every Lapikit component follows it.

You can use this action by calling `useTheme` with the desired theme name. For example, `useTheme('dark')` will apply the dark theme by adding the corresponding class to the HTML element.
Call `useTheme` with the name of the theme: `useTheme('dark')`, `useTheme('light')`, or `useTheme('system')` to follow the setting of the operating system (`prefers-color-scheme`).

<LazyRepl title="useThemeToggle.svelte" lang="svelte" content={useThemeToggleCode}>
<UseThemeToggle/>
</LazyRepl>

You can also customize the storage key in localStorage by passing a second argument to the `useTheme` function. By default, this key is set to `@lapikit/theme`.

See the [themes](/docs/customize) documentation to learn about all the available options for customizing Lapikit for your application.

## API References

| parameter | type | description | default |
| --------- | -------- | ------------------- | ---------------- |
| name | `string` | set the theme name | |
| key | `string` | set the storage key | `@lapikit/theme` |
| parameter | type | description | default |
| --------- | -------- | -------------------------------------------------- | ------- |
| name | `string` | The theme name: `'light'`, `'dark'`, `'system'`... | |

## Choose where Lapikit stores the theme

This allows Lapikit to preserve the selected theme between page reloads and browser sessions.
Lapikit keeps the selected theme between page reloads and browser sessions, under the `@lapikit/theme` key of `localStorage`. To change the key, pass it once to `createGlobalTheme`:

```svelte
<script>
import { createGlobalTheme } from 'lapikit/actions';

You can customize the storage key by passing a second argument to `useTheme`.
createGlobalTheme({ key: 'my-app/theme', name: 'system' });
</script>
```

This is particularly useful when:
`name` is the theme used when nothing is stored yet. A custom key is useful when:

- Managing multiple applications ;
- Isolating environments ;
- Testing different theme configurations ;
- Or integrating Lapikit into an existing storage architecture.

When `localStorage` is not available (private browsing, blocked storage), the theme still applies, without being kept.

## Avoid the flash of the wrong theme

With SSR, the page is painted before Svelte runs: the default theme shows for a moment, then the stored one. `themeScript()` returns an inline script that applies the stored theme before the first paint. Add it to the `<head>` of your root layout, with the same options as `createGlobalTheme`:

```svelte
<script>
import { themeScript } from 'lapikit/actions';
</script>

<svelte:head>
{@html themeScript({ key: 'my-app/theme', name: 'system' })}
</svelte:head>
```

> If your site sets a Content Security Policy that blocks inline scripts, allow this script with a nonce or a hash.

Because the theme is centralized, all Lapikit components automatically stay synchronized with the active theme across your Svelte or SvelteKit application.
9 changes: 3 additions & 6 deletions src/content/examples/components/field/preview.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<script lang="ts">
import type { FieldControlProps } from 'lapikit/components';

let country = $state('');
</script>

Expand All @@ -11,12 +13,7 @@
dirty={country !== ''}
onclear={() => (country = '')}
>
{#snippet children(control: {
id: string;
'aria-describedby': string | undefined;
'aria-invalid': true | undefined;
disabled: boolean;
})}
{#snippet children(control: FieldControlProps)}
<select
{...control}
bind:value={country}
Expand Down
32 changes: 3 additions & 29 deletions src/lib/server/npm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ import type { NpmStats } from '$lib/@types';
const CACHE_TTL = 15 * 60 * 1000;
const RETRY_DELAY = 60 * 1000;
const FETCH_TIMEOUT = 5000;
const FIRST_PUBLISH_DATE = '2025-04-19';
const DOWNLOADS_WINDOW_DAYS = 365;
const DAY_MS = 24 * 60 * 60 * 1000;

export const emptyNpmStats: NpmStats = {
version: { latest: '', insiders: '' },
Expand All @@ -22,44 +19,21 @@ function formatDownloads(n: number): string {
return String(n);
}

function toIsoDate(timestamp: number) {
return new Date(timestamp).toISOString().slice(0, 10);
}

function getDownloadRanges(from: string, to: number) {
const ranges: string[] = [];
let start = Date.parse(`${from}T00:00:00Z`);

while (start <= to) {
const end = Math.min(start + (DOWNLOADS_WINDOW_DAYS - 1) * DAY_MS, to);
ranges.push(`${toIsoDate(start)}:${toIsoDate(end)}`);
start = end + DAY_MS;
}

return ranges;
}

async function fetchJson(url: string) {
const response = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT) });
if (!response.ok) throw new Error(`${response.status} ${url}`);
return response.json();
}

async function collectStats(): Promise<NpmStats> {
const ranges = getDownloadRanges(FIRST_PUBLISH_DATE, Date.now());
const [packument, ...downloadPoints] = await Promise.all([
const [packument, downloadStats] = await Promise.all([
fetchJson('https://registry.npmjs.org/lapikit'),
...ranges.map((range) => fetchJson(`https://api.npmjs.org/downloads/point/${range}/lapikit`))
fetchJson('https://api.lapikit.dev/lapikit/nbDll')
]);

const distTagLatest = packument['dist-tags']?.latest;
const distTagInsider = packument['dist-tags']?.insiders;

const totalDownloads = downloadPoints.reduce(
(total: number, point: { downloads?: number }) => total + (point.downloads ?? 0),
0
);

return {
version: {
latest: distTagLatest ? `v${distTagLatest}` : '',
Expand All @@ -69,7 +43,7 @@ async function collectStats(): Promise<NpmStats> {
latest: packument.time?.[distTagLatest] ?? '',
insiders: packument.time?.[distTagInsider] ?? ''
},
downloads: formatDownloads(totalDownloads)
downloads: formatDownloads(downloadStats.downloads ?? 0)
};
}

Expand Down
Loading