lino-ui 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +216 -0
- package/README.md +29 -2
- package/bin/lino.js +168 -0
- package/docs/agents.md +230 -0
- package/docs/button-group.md +76 -0
- package/docs/button.md +117 -0
- package/docs/colors.md +72 -0
- package/docs/dialog.md +109 -0
- package/docs/dropdown-menu.md +201 -0
- package/docs/icons.md +20 -0
- package/docs/input.md +95 -0
- package/docs/introduction.md +45 -0
- package/docs/kbd.md +61 -0
- package/docs/label.md +38 -0
- package/docs/llms-full.txt +2257 -0
- package/docs/llms.txt +66 -0
- package/docs/menubar.md +82 -0
- package/docs/picture.md +66 -0
- package/docs/plaque.md +49 -0
- package/docs/principles.md +100 -0
- package/docs/separator.md +45 -0
- package/docs/slider.md +89 -0
- package/docs/sounds.md +7 -0
- package/docs/spinner.md +48 -0
- package/docs/surfaces.md +20 -0
- package/docs/switch.md +62 -0
- package/docs/tag.md +54 -0
- package/docs/toggle-group.md +102 -0
- package/docs/toggle.md +51 -0
- package/docs/toolbar.md +88 -0
- package/docs/tooltip.md +75 -0
- package/docs/typography.md +21 -0
- package/fonts/alegreya-sans-sc-cyrillic-500-normal.woff2 +0 -0
- package/fonts/alegreya-sans-sc-cyrillic-700-normal.woff2 +0 -0
- package/fonts/alegreya-sans-sc-cyrillic-800-normal.woff2 +0 -0
- package/fonts/alegreya-sans-sc-latin-500-normal.woff2 +0 -0
- package/fonts/alegreya-sans-sc-latin-700-normal.woff2 +0 -0
- package/fonts/alegreya-sans-sc-latin-800-normal.woff2 +0 -0
- package/fonts/inter-cyrillic-wght-normal.woff2 +0 -0
- package/fonts/inter-latin-wght-normal.woff2 +0 -0
- package/fonts/td-volshebstvo.woff2 +0 -0
- package/package.json +65 -4
- package/site/apple-touch-icon.png +0 -0
- package/site/assets/accent-click-1-C4yMKlL7.mp3 +0 -0
- package/site/assets/accent-click-2-Dn01blTK.mp3 +0 -0
- package/site/assets/accent-hover-1-DX_94LsD.mp3 +0 -0
- package/site/assets/accent-hover-2-CwkP6tLp.mp3 +0 -0
- package/site/assets/accent-hover-3-EqmGpEzM.mp3 +0 -0
- package/site/assets/index-BgVvspgp.css +1 -0
- package/site/assets/index-DGqYYe1O.js +1102 -0
- package/site/assets/owl-1-DMSClExH.mp3 +0 -0
- package/site/assets/owl-2-DadTAnqX.mp3 +0 -0
- package/site/assets/send-click-1-D8oZSriI.mp3 +0 -0
- package/site/assets/send-click-2-Cuq0Ywc4.mp3 +0 -0
- package/site/assets/small-click-B71aagwY.mp3 +0 -0
- package/site/assets/support-accent-BUGjkaKV.mp3 +0 -0
- package/site/assets/view-close-CW4FuJmT.mp3 +0 -0
- package/site/assets/view-open-yAP5VPW3.mp3 +0 -0
- package/site/ds/agents/llms.txt +230 -0
- package/site/ds/button/llms.txt +117 -0
- package/site/ds/button-group/llms.txt +76 -0
- package/site/ds/colors/llms.txt +72 -0
- package/site/ds/dialog/llms.txt +109 -0
- package/site/ds/dropdown-menu/llms.txt +201 -0
- package/site/ds/icons/llms.txt +20 -0
- package/site/ds/input/llms.txt +95 -0
- package/site/ds/introduction/llms.txt +45 -0
- package/site/ds/kbd/llms.txt +61 -0
- package/site/ds/label/llms.txt +38 -0
- package/site/ds/llms-full.txt +2257 -0
- package/site/ds/llms.txt +66 -0
- package/site/ds/menubar/llms.txt +82 -0
- package/site/ds/picture/llms.txt +66 -0
- package/site/ds/plaque/llms.txt +49 -0
- package/site/ds/principles/llms.txt +100 -0
- package/site/ds/separator/llms.txt +45 -0
- package/site/ds/skills/lino-texture/SKILL.md +73 -0
- package/site/ds/skills/lino-texture/references/nslice.md +116 -0
- package/site/ds/skills/lino-texture/references/texture.md +74 -0
- package/site/ds/skills/lino-ui/SKILL.md +49 -0
- package/site/ds/slider/llms.txt +89 -0
- package/site/ds/sounds/llms.txt +7 -0
- package/site/ds/spinner/llms.txt +48 -0
- package/site/ds/surfaces/llms.txt +20 -0
- package/site/ds/switch/llms.txt +62 -0
- package/site/ds/tag/llms.txt +54 -0
- package/site/ds/toggle/llms.txt +51 -0
- package/site/ds/toggle-group/llms.txt +102 -0
- package/site/ds/toolbar/llms.txt +88 -0
- package/site/ds/tooltip/llms.txt +75 -0
- package/site/ds/typography/llms.txt +21 -0
- package/site/favicon-16x16.png +0 -0
- package/site/favicon-32x32.png +0 -0
- package/site/favicon.ico +0 -0
- package/site/fonts/alegreya-sans-sc-cyrillic-500-normal.woff2 +0 -0
- package/site/fonts/alegreya-sans-sc-cyrillic-700-normal.woff2 +0 -0
- package/site/fonts/alegreya-sans-sc-cyrillic-800-normal.woff2 +0 -0
- package/site/fonts/alegreya-sans-sc-latin-500-normal.woff2 +0 -0
- package/site/fonts/alegreya-sans-sc-latin-700-normal.woff2 +0 -0
- package/site/fonts/alegreya-sans-sc-latin-800-normal.woff2 +0 -0
- package/site/fonts/inter-cyrillic-wght-normal.woff2 +0 -0
- package/site/fonts/inter-latin-wght-normal.woff2 +0 -0
- package/site/fonts/td-volshebstvo.woff2 +0 -0
- package/site/icon-192.png +0 -0
- package/site/icon-512.png +0 -0
- package/site/index.html +19 -0
- package/site/map/avatars/bsodpanic.jpg +0 -0
- package/site/map/avatars/uberalien.jpg +0 -0
- package/site/map/avatars/xm1ller.jpg +0 -0
- package/site/site.webmanifest +11 -0
- package/skills/lino-texture/SKILL.md +73 -0
- package/skills/lino-texture/references/nslice.md +116 -0
- package/skills/lino-texture/references/texture.md +74 -0
- package/skills/lino-ui/SKILL.md +49 -0
- package/src/ds/PRINCIPLES.md +98 -0
- package/src/ds/art/plaque-art.js +2 -0
- package/src/ds/icon-bake.js +39 -0
- package/src/ds/icon.js +47 -0
- package/src/ds/icons/paths.js +28 -0
- package/src/ds/keys.js +40 -0
- package/src/ds/nslice.js +204 -0
- package/src/ds/rough.js +117 -0
- package/src/ds/sounds/accent-click-1.mp3 +0 -0
- package/src/ds/sounds/accent-click-2.mp3 +0 -0
- package/src/ds/sounds/accent-hover-1.mp3 +0 -0
- package/src/ds/sounds/accent-hover-2.mp3 +0 -0
- package/src/ds/sounds/accent-hover-3.mp3 +0 -0
- package/src/ds/sounds/owl-1.mp3 +0 -0
- package/src/ds/sounds/owl-2.mp3 +0 -0
- package/src/ds/sounds/send-click-1.mp3 +0 -0
- package/src/ds/sounds/send-click-2.mp3 +0 -0
- package/src/ds/sounds/small-click.mp3 +0 -0
- package/src/ds/sounds/small-hover.mp3 +0 -0
- package/src/ds/sounds/support-accent.mp3 +0 -0
- package/src/ds/sounds/view-close.mp3 +0 -0
- package/src/ds/sounds/view-open.mp3 +0 -0
- package/src/ds/sounds.js +234 -0
- package/src/ds/texture.js +379 -0
- package/src/ds/tier.js +165 -0
- package/src/lib/components/ui/button/button.svelte +49 -0
- package/src/lib/components/ui/button/index.js +13 -0
- package/src/lib/components/ui/button-group/button-group.svelte +11 -0
- package/src/lib/components/ui/button-group/index.js +7 -0
- package/src/lib/components/ui/dialog/dialog-close.svelte +11 -0
- package/src/lib/components/ui/dialog/dialog-content.svelte +40 -0
- package/src/lib/components/ui/dialog/dialog-description.svelte +17 -0
- package/src/lib/components/ui/dialog/dialog-footer.svelte +30 -0
- package/src/lib/components/ui/dialog/dialog-header.svelte +18 -0
- package/src/lib/components/ui/dialog/dialog-overlay.svelte +18 -0
- package/src/lib/components/ui/dialog/dialog-portal.svelte +7 -0
- package/src/lib/components/ui/dialog/dialog-title.svelte +17 -0
- package/src/lib/components/ui/dialog/dialog-trigger.svelte +11 -0
- package/src/lib/components/ui/dialog/dialog.svelte +7 -0
- package/src/lib/components/ui/dialog/index.js +34 -0
- package/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte +16 -0
- package/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +30 -0
- package/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte +34 -0
- package/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte +18 -0
- package/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte +7 -0
- package/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte +25 -0
- package/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte +20 -0
- package/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte +7 -0
- package/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte +16 -0
- package/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +26 -0
- package/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte +17 -0
- package/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte +18 -0
- package/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte +29 -0
- package/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +25 -0
- package/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte +7 -0
- package/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +7 -0
- package/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte +7 -0
- package/src/lib/components/ui/dropdown-menu/index.js +54 -0
- package/src/lib/components/ui/input/index.js +7 -0
- package/src/lib/components/ui/input/input.svelte +19 -0
- package/src/lib/components/ui/kbd/index.js +10 -0
- package/src/lib/components/ui/kbd/kbd-group.svelte +18 -0
- package/src/lib/components/ui/kbd/kbd.svelte +22 -0
- package/src/lib/components/ui/label/index.js +7 -0
- package/src/lib/components/ui/label/label.svelte +17 -0
- package/src/lib/components/ui/menubar/index.js +55 -0
- package/src/lib/components/ui/menubar/menubar-checkbox-item.svelte +32 -0
- package/src/lib/components/ui/menubar/menubar-content.svelte +33 -0
- package/src/lib/components/ui/menubar/menubar-group-heading.svelte +18 -0
- package/src/lib/components/ui/menubar/menubar-group.svelte +10 -0
- package/src/lib/components/ui/menubar/menubar-item.svelte +25 -0
- package/src/lib/components/ui/menubar/menubar-label.svelte +20 -0
- package/src/lib/components/ui/menubar/menubar-menu.svelte +7 -0
- package/src/lib/components/ui/menubar/menubar-portal.svelte +7 -0
- package/src/lib/components/ui/menubar/menubar-radio-group.svelte +11 -0
- package/src/lib/components/ui/menubar/menubar-radio-item.svelte +28 -0
- package/src/lib/components/ui/menubar/menubar-separator.svelte +17 -0
- package/src/lib/components/ui/menubar/menubar-shortcut.svelte +18 -0
- package/src/lib/components/ui/menubar/menubar-sub-content.svelte +29 -0
- package/src/lib/components/ui/menubar/menubar-sub-trigger.svelte +25 -0
- package/src/lib/components/ui/menubar/menubar-sub.svelte +7 -0
- package/src/lib/components/ui/menubar/menubar-trigger.svelte +18 -0
- package/src/lib/components/ui/menubar/menubar.svelte +17 -0
- package/src/lib/components/ui/picture/index.js +7 -0
- package/src/lib/components/ui/picture/picture.svelte +21 -0
- package/src/lib/components/ui/plaque/index.js +7 -0
- package/src/lib/components/ui/plaque/plaque.svelte +16 -0
- package/src/lib/components/ui/separator/index.js +7 -0
- package/src/lib/components/ui/separator/separator.svelte +19 -0
- package/src/lib/components/ui/slider/index.js +7 -0
- package/src/lib/components/ui/slider/slider.svelte +31 -0
- package/src/lib/components/ui/spinner/index.js +1 -0
- package/src/lib/components/ui/spinner/spinner.svelte +16 -0
- package/src/lib/components/ui/swatch/index.js +10 -0
- package/src/lib/components/ui/swatch/swatch-group.svelte +21 -0
- package/src/lib/components/ui/swatch/swatch.svelte +32 -0
- package/src/lib/components/ui/switch/index.js +7 -0
- package/src/lib/components/ui/switch/switch.svelte +23 -0
- package/src/lib/components/ui/tag/index.js +7 -0
- package/src/lib/components/ui/tag/tag.svelte +11 -0
- package/src/lib/components/ui/toggle/index.js +11 -0
- package/src/lib/components/ui/toggle/toggle.svelte +28 -0
- package/src/lib/components/ui/toggle-group/index.js +10 -0
- package/src/lib/components/ui/toggle-group/toggle-group-item.svelte +29 -0
- package/src/lib/components/ui/toggle-group/toggle-group.svelte +82 -0
- package/src/lib/components/ui/toolbar/index.js +7 -0
- package/src/lib/components/ui/toolbar/toolbar.svelte +26 -0
- package/src/lib/components/ui/tooltip/index.js +19 -0
- package/src/lib/components/ui/tooltip/tooltip-content.svelte +44 -0
- package/src/lib/components/ui/tooltip/tooltip-portal.svelte +7 -0
- package/src/lib/components/ui/tooltip/tooltip-provider.svelte +7 -0
- package/src/lib/components/ui/tooltip/tooltip-trigger.svelte +7 -0
- package/src/lib/components/ui/tooltip/tooltip.svelte +7 -0
- package/src/lib/cut.js +12 -0
- package/src/lib/index.js +8 -0
- package/src/lib/lino.css +324 -0
- package/src/lib/press.js +40 -0
- package/src/lib/setup.js +7 -0
- package/src/lib/surfaces.js +3 -0
- package/src/lib/utils.js +7 -0
- package/styles.css +2 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# Building interface with lino-ui
|
|
2
|
+
|
|
3
|
+
For agents (and people) who build screens with `lino-ui`: Svelte 5 components in lino's look, paper, ink, one
|
|
4
|
+
accent, every edge cut by hand. Read this file whole before the first piece of UI; it is short, and every rule in it
|
|
5
|
+
comes from review. Paths below are relative to the package (`node_modules/lino-ui/`).
|
|
6
|
+
|
|
7
|
+
## At the start of every session
|
|
8
|
+
|
|
9
|
+
1. Start the documentation site and tell the user where it is:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx lino docs --detach
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
It prints a local URL (the same one while it runs; it starts once). Say to the user, in their language, something
|
|
16
|
+
like: "The lino-ui docs are open at http://localhost:4977: every component with live examples, its props and
|
|
17
|
+
these rules." Do it again whenever you start the app's dev server, if it is not running.
|
|
18
|
+
2. Read this file. Before you use a component, read its page: `docs/<component>.md` (`docs/button.md`), or the same at
|
|
19
|
+
`<docs URL>/ds/<component>/llms.txt`. The index of every page is `docs/llms.txt`.
|
|
20
|
+
|
|
21
|
+
## Setting up an app
|
|
22
|
+
|
|
23
|
+
Svelte 5 with Vite (SvelteKit or plain). Once:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm i lino-ui
|
|
27
|
+
npx lino init
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`lino init` gives the project's agents this guide: the skills (`.claude/skills`, pointers into the package, so they
|
|
31
|
+
update with it), a block in `AGENTS.md` and `CLAUDE.md`, and a Claude Code hook that starts the docs with each session.
|
|
32
|
+
Then, in the app's entry (SvelteKit: the root `+layout.svelte`):
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
import "lino-ui/styles.css"; // tokens, fonts, the kit's look
|
|
36
|
+
import "lino-ui/setup"; // the rough cuts, the press, the interface sounds
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The page's background is the paper: `body { background: var(--color-background); color: var(--color-text); }`.
|
|
40
|
+
|
|
41
|
+
Three themes, only the colours change: Solar (light, the default), and the dark Ink and Lunar on
|
|
42
|
+
`<html data-theme="ink">` / `"lunar"`. Set the attribute before the first paint (a line of script in `<head>`), so a
|
|
43
|
+
dark theme never flashes paper. Everything you build follows by itself as long as it takes its colours from tokens.
|
|
44
|
+
|
|
45
|
+
## Building from a prompt
|
|
46
|
+
|
|
47
|
+
1. **Read the request as surfaces, controls, content.** Surfaces are what things sit on (a panel, a menu, a dialog,
|
|
48
|
+
a card). Controls are what the user acts on. Content is text, pictures, numbers. Icons belong to controls.
|
|
49
|
+
2. **Pick a component for every control and surface** (the table below). Compose; never restyle a component into
|
|
50
|
+
another one.
|
|
51
|
+
3. **Lay it out on the 4px grid.** Spaces are `--space-1` 4px … `--space-8` 32px. Hug the content, the same padding
|
|
52
|
+
on every side, one height and one centre line for everything on a bar.
|
|
53
|
+
4. **One surface per level.** A control on a plate uses that plate; it never brings its own fill inside it. Group by
|
|
54
|
+
space first; a separator only between real groups.
|
|
55
|
+
5. **Check the list of don'ts**, then look at it in the browser (see Verify).
|
|
56
|
+
|
|
57
|
+
### Which component
|
|
58
|
+
|
|
59
|
+
| The user needs | Use |
|
|
60
|
+
|---|---|
|
|
61
|
+
| An action | `Button`: `variant="action"` on a surface, `"panel"` straight on the paper, `"accent"` for the one primary action of a view, `"ghost"` for quiet ones |
|
|
62
|
+
| An action shown as an icon | `Button size="icon"` (`icon-sm`, `icon-lg`) with `aria-label`, inside a `Tooltip` with its name and shortcut |
|
|
63
|
+
| Several actions joined | `ButtonGroup` |
|
|
64
|
+
| On / off | `Switch` with a `Label` |
|
|
65
|
+
| One of a few, all visible | `ToggleGroup` (a track); a pressed state on one button: `Toggle` |
|
|
66
|
+
| A set of tools | `Toolbar` holding a `ToggleGroup variant="bare" size="icon"` |
|
|
67
|
+
| One of many, or actions behind a button | `DropdownMenu` (radio items for a choice, `.lino-grid` for tiles) |
|
|
68
|
+
| An app's menus | `Menubar` |
|
|
69
|
+
| Text in | `Input` with a `Label` |
|
|
70
|
+
| A number in a range | `Slider` |
|
|
71
|
+
| A decision that stops the flow | `Dialog` (title, description, footer with the actions) |
|
|
72
|
+
| A shortcut | `Kbd keys="mod shift z"` |
|
|
73
|
+
| Something is loading | `Spinner` with `aria-label` |
|
|
74
|
+
| A rank or tier | `Tag`, a framed `Picture`, a `Plaque` |
|
|
75
|
+
| A break between groups | `Separator` (rarely) |
|
|
76
|
+
| A panel, card or section of your own | a `div` with `{@attach cut('plate')}` and `--surface-bg` (Texture, below) |
|
|
77
|
+
|
|
78
|
+
```svelte
|
|
79
|
+
<script>
|
|
80
|
+
import { Button } from "lino-ui/button";
|
|
81
|
+
import * as Tooltip from "lino-ui/tooltip";
|
|
82
|
+
import { icon } from "lino-ui";
|
|
83
|
+
</script>
|
|
84
|
+
|
|
85
|
+
<Tooltip.Provider>
|
|
86
|
+
<Tooltip.Root>
|
|
87
|
+
<Tooltip.Trigger>
|
|
88
|
+
{#snippet child({ props })}
|
|
89
|
+
<Button {...props} size="icon" aria-label="Search">{@html icon("search")}</Button>
|
|
90
|
+
{/snippet}
|
|
91
|
+
</Tooltip.Trigger>
|
|
92
|
+
<Tooltip.Content sideOffset={8}>Search</Tooltip.Content>
|
|
93
|
+
</Tooltip.Root>
|
|
94
|
+
</Tooltip.Provider>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Do
|
|
98
|
+
|
|
99
|
+
- Use the kit's component whenever there is one, as its page shows it. Its props are the API; `class` adds layout
|
|
100
|
+
(margins, grid placement), never a new look.
|
|
101
|
+
- Colours, type, space and motion are tokens (`src/ds/tokens.css`): `--color-background` (paper), `--color-text`
|
|
102
|
+
(ink), `--color-surface` (plates), `--color-accent` (on, primary, focus), `--color-muted` (captions, shortcuts),
|
|
103
|
+
`--color-border`, `--color-divider`. A hover, a pressed state or a tint is `color-mix()` of tokens where it is used:
|
|
104
|
+
`color-mix(in srgb, var(--color-text) 6%, var(--color-surface))`.
|
|
105
|
+
- The chrome speaks Alegreya Sans SC (`--font-display`): buttons, menus, labels, titles. Inter (`--font-text`) only for
|
|
106
|
+
running text. Sizes `--text-xs` 14 … `--text-2xl` 56px.
|
|
107
|
+
- Accent orange means "on" or "primary": one accent action per view, a switch that is on, the focus ring.
|
|
108
|
+
- Every white surface on the paper gets the border: `--surface-border: var(--color-border)`.
|
|
109
|
+
- Name every icon-only control (`aria-label`) and show the name, with the shortcut, in a tooltip.
|
|
110
|
+
- Keep the keyboard: every control is reachable, focus rings stay whole (`outline: var(--focus-ring)`).
|
|
111
|
+
- Motion: short fades and 4px slides with `--dur` and `--ease-out`; a press shrinks by `--press` (the kit does it).
|
|
112
|
+
|
|
113
|
+
## Don't
|
|
114
|
+
|
|
115
|
+
- **No CSS `border`, `border-radius`, `box-shadow` or `filter`.** Not on anything. No pills, no rounded corners, no
|
|
116
|
+
circles in rectangles, no elevation. A surface separates from the paper by its fill and a cut border.
|
|
117
|
+
- No hex colours and no new colour tokens: `hsl()` tokens and `color-mix()` of them. A colour written as a value
|
|
118
|
+
(`white`, `#fff`, `hsl(0 0% 100%)`) breaks the dark themes; `--color-surface` is white only in Solar.
|
|
119
|
+
- No translucent colour on an icon: its stroke darkens where it overlaps. Use an opaque token (`--color-muted`).
|
|
120
|
+
- No inline SVG icons, icon fonts, lucide or emoji as icons. Icons are `icon(name)`; a new one is baked (Icons).
|
|
121
|
+
- No decoration: no glyph that means nothing, no divider for looks, no control the screen doesn't need now.
|
|
122
|
+
- Hover never moves anything: one tint step, a quiet label to full ink. No scale, no translate on hover.
|
|
123
|
+
- Layout never lives only on an open state (`[data-state=open]`): what fades out would jump.
|
|
124
|
+
- Never copy a mockup's frames, badges, numerals or icons as pictures. From a mockup take only photos and
|
|
125
|
+
illustrations; everything else is built with the kit.
|
|
126
|
+
- Never outline text. Text is real text.
|
|
127
|
+
|
|
128
|
+
## Texture: when to cut, and with what
|
|
129
|
+
|
|
130
|
+
Every surface and border in lino is cut, like paper trimmed with scissors. A cut is a vector outline displaced by
|
|
131
|
+
noise: no filter, no image of an edge. Decide by what the thing is:
|
|
132
|
+
|
|
133
|
+
| It is | Cut? | How |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| A kit component (button, menu, dialog, toolbar, tag…) | Already cut | Nothing. Don't add a border or a fill to it |
|
|
136
|
+
| A panel, card, section, popover, list box or code block you compose, 32px or more | Yes | `{@attach cut('plate')}` from `lino-ui`; set `--surface-bg`, and `--surface-border` for a border |
|
|
137
|
+
| A strip, chip or field of your own, 16–32px high | Yes | `{@attach cut('chip')}` |
|
|
138
|
+
| A label with a thick border | Yes | `{@attach cut('tag')}`, or the kit's `Tag` |
|
|
139
|
+
| A badge, frame, medal, tile, ticket, any shape that is not a plain rectangle, a piece of art | Yes | Skill **lino-texture**: a fixed shape is cut with `texturize` (or baked); one that resizes is an n-slice surface |
|
|
140
|
+
| A new icon | Yes, baked | `npx lino icon <file.svg>` (Icons) |
|
|
141
|
+
| A photo or an illustration | Not the picture | Clip it with a cut shape (`clip-path`), the picture untouched; or the kit's `Picture` |
|
|
142
|
+
| Text | Never | Real text on top of the cut |
|
|
143
|
+
| A layout wrapper with no fill (a row, a grid, a column) | No | Nothing to cut: it has no edge |
|
|
144
|
+
| A plate inside a plate | No | One surface per level: drop the inner fill, group by space |
|
|
145
|
+
| The page's paper | No | `--color-background` on `body` |
|
|
146
|
+
| A divider | Already cut | `Separator` (a cut strip, `--rough-rule`) |
|
|
147
|
+
| A focus ring | No | `outline: var(--focus-ring)`, never clipped (the fill sits on a layer behind the content) |
|
|
148
|
+
|
|
149
|
+
A plate of your own:
|
|
150
|
+
|
|
151
|
+
```svelte
|
|
152
|
+
<script>
|
|
153
|
+
import { cut } from "lino-ui";
|
|
154
|
+
</script>
|
|
155
|
+
|
|
156
|
+
<section class="card" {@attach cut("plate")}>…</section>
|
|
157
|
+
|
|
158
|
+
<style>
|
|
159
|
+
.card { --surface-bg: var(--color-surface); --surface-border: var(--color-border); padding: var(--space-6); }
|
|
160
|
+
</style>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Neighbouring plates get different seeds by themselves. A state changes `--surface-bg` / `--surface-border` only; the
|
|
164
|
+
outline never changes.
|
|
165
|
+
|
|
166
|
+
## Icons
|
|
167
|
+
|
|
168
|
+
`icon(name)` from `lino-ui` returns the `<svg>` markup: `{@html icon("search")}`. It is coloured by `currentColor`,
|
|
169
|
+
sized by its glyph, and in the kit's buttons, toggles and menu rows it is sized and weighted for you: beside a label
|
|
170
|
+
`.8em`, 12px from it, as heavy as the label's letters; alone 20, 24 or 26px on a 32, 40 or 56px control. `ICON_NAMES()`
|
|
171
|
+
lists the set.
|
|
172
|
+
|
|
173
|
+
A new icon is never drawn in code. Get the vector (a Figma export with strokes, not outlined) and bake it:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
npx lino icon design/icons/*.svg --out src/lib/icons.js
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
It cuts every glyph at the one icon weight with the hand-cut texture. Import the output once in the app's entry
|
|
180
|
+
(`import "./lib/icons.js"`): it registers the icons, and `icon("<file name>")` draws them. Skill **lino-ui**, Icons.
|
|
181
|
+
|
|
182
|
+
## Skills: which one when
|
|
183
|
+
|
|
184
|
+
| Skill | Use it for |
|
|
185
|
+
|---|---|
|
|
186
|
+
| **lino-ui** | Any interface work with the kit: a screen, a panel, a form, a menu, a dialog, choosing a component, an icon, "in lino's style" |
|
|
187
|
+
| **lino-texture** | A shape the kit doesn't have: a badge, a frame, a plaque, a tile, a decorative plate, art, a surface that stretches (n-slice) |
|
|
188
|
+
|
|
189
|
+
`npx lino init` installs them for Claude Code (`.claude/skills`). Any other agent reads them from
|
|
190
|
+
`skills/<name>/SKILL.md`.
|
|
191
|
+
|
|
192
|
+
## Verify
|
|
193
|
+
|
|
194
|
+
Look at it in the browser before you call it done: a zoomed screenshot of every edge (cut, never straight, no seams)
|
|
195
|
+
and of the spacing; measure paddings on the cut edge, not the box; tab through it with the keyboard; check that every
|
|
196
|
+
icon-only control has a name. Then tell the user what to look at, and that the docs are at the URL `lino docs` gave.
|
|
197
|
+
|
|
198
|
+
## The CLI
|
|
199
|
+
|
|
200
|
+
| Command | Does |
|
|
201
|
+
|---|---|
|
|
202
|
+
| `npx lino docs [--port 4977] [--detach]` | Serves the documentation site; `--detach` leaves it running and prints its URL |
|
|
203
|
+
| `npx lino init` | Gives the project's agents this guide: skills, the AGENTS.md / CLAUDE.md block, the session hook |
|
|
204
|
+
| `npx lino icon <svg…> [--out file.js]` | Bakes SVG vectors into hand-cut icons for `icon()` |
|
|
205
|
+
| `npx lino surface <specs.mjs> <out.js>` | Bakes n-slice surfaces (skill lino-texture) |
|
|
206
|
+
|
|
207
|
+
## Where things are
|
|
208
|
+
|
|
209
|
+
| Path | What |
|
|
210
|
+
|---|---|
|
|
211
|
+
| `docs/` | Every page of the site as Markdown: `llms.txt` (the index), `llms-full.txt` (all), `<page>.md` |
|
|
212
|
+
| `skills/` | The agent skills |
|
|
213
|
+
| `src/ds/PRINCIPLES.md` | The full rules, with their reasons |
|
|
214
|
+
| `src/lib/components/ui/` | The components' source |
|
|
215
|
+
| `src/lib/lino.css`, `src/ds/tokens.css` | The look and the tokens |
|
|
216
|
+
| `site/` | The documentation site that `lino docs` serves |
|
package/README.md
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
-
#
|
|
1
|
+
# lino-ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Svelte 5 components in lino's look: paper, ink, one accent, and every edge cut by hand, like paper trimmed with
|
|
4
|
+
scissors. Focus, keyboard and screen-reader behaviour come with them.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm i lino-ui
|
|
8
|
+
npx lino init # the guide, the skills and a session hook for the project's agents
|
|
9
|
+
npx lino docs # the documentation site, on http://localhost:4977
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
In the app's entry (SvelteKit: the root `+layout.svelte`):
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
import "lino-ui/styles.css";
|
|
16
|
+
import "lino-ui/setup";
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```svelte
|
|
20
|
+
<script>
|
|
21
|
+
import { Button } from "lino-ui/button";
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<Button variant="accent">Publish</Button>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- [AGENTS.md](AGENTS.md): how to build with it, what never to do, when to cut a texture, which skill when. For agents
|
|
28
|
+
and people alike.
|
|
29
|
+
- `docs/`: every page of the documentation as Markdown (`docs/llms.txt` is the index).
|
|
30
|
+
- `skills/`: lino-ui (building interface) and lino-texture (new hand-cut shapes).
|
package/bin/lino.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The lino-ui command line (AGENTS.md, The CLI):
|
|
3
|
+
// lino docs [--port 4977] [--detach] serve the documentation site (./site); --detach leaves it running
|
|
4
|
+
// lino init [--no-hook] give the project's agents the guide, the skills and the session hook
|
|
5
|
+
// lino icon <svg…> [--out file.js] bake vectors into hand-cut icons for icon()
|
|
6
|
+
// lino surface <specs.mjs> <out.js> bake n-slice surfaces
|
|
7
|
+
// No dependencies: Node's own http, fs, child_process.
|
|
8
|
+
import { createServer } from 'node:http';
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { basename, dirname, extname, join, relative, resolve, sep } from 'node:path';
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
13
|
+
|
|
14
|
+
const PKG = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
15
|
+
const { name: NAME, version: VERSION } = JSON.parse(readFileSync(join(PKG, 'package.json'), 'utf8'));
|
|
16
|
+
const args = process.argv.slice(2);
|
|
17
|
+
const cmd = args.shift();
|
|
18
|
+
const flag = (k, d) => { const i = args.indexOf(k); return i < 0 ? d : args.splice(i, 2)[1]; };
|
|
19
|
+
const has = (k) => { const i = args.indexOf(k); return i >= 0 && args.splice(i, 1).length > 0; };
|
|
20
|
+
const die = (msg) => { console.error(msg); process.exit(1); };
|
|
21
|
+
|
|
22
|
+
// ---------- docs: the site, on the first free port from 4977; the same server again while it runs ----------
|
|
23
|
+
const TYPES = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.txt': 'text/plain',
|
|
24
|
+
'.md': 'text/markdown', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg', '.ico': 'image/x-icon', '.woff2': 'font/woff2',
|
|
25
|
+
'.mp3': 'audio/mpeg', '.webmanifest': 'application/manifest+json' };
|
|
26
|
+
const SITE = join(PKG, 'site');
|
|
27
|
+
const say = (url) => console.log(`lino-ui docs: ${url}\nThe documentation site (every component live, its examples and props, the rules for building with lino-ui) is running there. Tell the user they can open it.`);
|
|
28
|
+
|
|
29
|
+
// what answers on a port: this package's docs server, another, or nothing
|
|
30
|
+
async function probe(port) {
|
|
31
|
+
try {
|
|
32
|
+
const r = await fetch(`http://localhost:${port}/__lino`, { signal: AbortSignal.timeout(500) });
|
|
33
|
+
const info = r.ok ? await r.json().catch(() => null) : null;
|
|
34
|
+
return info?.site === SITE ? 'ours' : 'taken';
|
|
35
|
+
} catch (e) { return e.cause?.code === 'ECONNREFUSED' ? 'free' : 'taken'; }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function docs() {
|
|
39
|
+
const first = +flag('--port', 4977), detach = has('--detach');
|
|
40
|
+
if (!existsSync(join(SITE, 'index.html'))) die(`no site in ${SITE}: is ${NAME} built?`);
|
|
41
|
+
for (let port = first; port < first + 20; port++) {
|
|
42
|
+
const state = await probe(port);
|
|
43
|
+
if (state === 'ours') return say(`http://localhost:${port}/`);
|
|
44
|
+
if (state === 'taken') continue;
|
|
45
|
+
if (detach) {
|
|
46
|
+
spawn(process.execPath, [fileURLToPath(import.meta.url), 'docs', '--port', String(port)], { detached: true, stdio: 'ignore' }).unref();
|
|
47
|
+
for (let i = 0; i < 50; i++) {
|
|
48
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
49
|
+
if (await probe(port) === 'ours') return say(`http://localhost:${port}/`);
|
|
50
|
+
}
|
|
51
|
+
die(`the docs server did not start on ${port}`);
|
|
52
|
+
}
|
|
53
|
+
return serve(port);
|
|
54
|
+
}
|
|
55
|
+
die(`no free port from ${first} to ${first + 19}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function serve(port) {
|
|
59
|
+
createServer((req, res) => {
|
|
60
|
+
const path = decodeURIComponent(new URL(req.url, 'http://x').pathname);
|
|
61
|
+
if (path === '/__lino') return res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ name: NAME, version: VERSION, site: SITE }));
|
|
62
|
+
let file = resolve(SITE, '.' + path);
|
|
63
|
+
if (!file.startsWith(SITE + sep) && file !== SITE) return res.writeHead(403).end();
|
|
64
|
+
if (existsSync(file) && statSync(file).isDirectory()) file = join(file, 'index.html');
|
|
65
|
+
if (!existsSync(file)) return res.writeHead(404, { 'content-type': 'text/plain' }).end('not found');
|
|
66
|
+
const type = TYPES[extname(file)] ?? 'application/octet-stream';
|
|
67
|
+
res.writeHead(200, { 'content-type': type.startsWith('text/') || type.includes('json') ? `${type}; charset=utf-8` : type });
|
|
68
|
+
res.end(readFileSync(file));
|
|
69
|
+
}).listen(port, () => say(`http://localhost:${port}/`));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------- init: the guide, the skills and the hook in the project (run again after an update) ----------
|
|
73
|
+
const START = '<!-- lino-ui:start (written by `npx lino init`; run it again after updating lino-ui) -->', END = '<!-- lino-ui:end -->';
|
|
74
|
+
|
|
75
|
+
function init() {
|
|
76
|
+
const root = process.cwd(), pkg = relative(root, PKG).split(sep).join('/'), noHook = has('--no-hook');
|
|
77
|
+
const block = `${START}
|
|
78
|
+
## Interface: lino-ui
|
|
79
|
+
|
|
80
|
+
The interface is built with lino-ui. Before any UI work read \`${pkg}/AGENTS.md\` and follow it: what to do and what
|
|
81
|
+
never to do, which component for which need, when to cut a texture.
|
|
82
|
+
|
|
83
|
+
- At the start of a session, and whenever you start the app's dev server, run \`npx lino docs --detach\` and tell the
|
|
84
|
+
user the URL it prints: the documentation site, with every component, its examples and the rules.
|
|
85
|
+
- Skills: **lino-ui** for any interface work, **lino-texture** for a new hand-cut shape. Their files:
|
|
86
|
+
\`${pkg}/skills/\`.
|
|
87
|
+
${END}`;
|
|
88
|
+
const put = (file, text) => { mkdirSync(dirname(file), { recursive: true }); writeFileSync(file, text); console.log('wrote', relative(root, file)); };
|
|
89
|
+
const withBlock = (text) => text.includes(START) ? text.replace(new RegExp(`${START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${END}`), block) : `${text.trimEnd()}${text.trim() ? '\n\n' : ''}${block}\n`;
|
|
90
|
+
|
|
91
|
+
// the skills: Claude Code finds them in .claude/skills; each is a pointer to the package's file, so it updates with it
|
|
92
|
+
for (const name of readdirSync(join(PKG, 'skills'))) {
|
|
93
|
+
const src = readFileSync(join(PKG, 'skills', name, 'SKILL.md'), 'utf8'), front = src.match(/^---\n[\s\S]*?\n---\n/)[0];
|
|
94
|
+
put(join(root, '.claude/skills', name, 'SKILL.md'), `${front}
|
|
95
|
+
This skill ships with lino-ui, and lives in the package so it is always the installed version's. Read
|
|
96
|
+
\`${pkg}/skills/${name}/SKILL.md\` now and follow it; its references are beside it.
|
|
97
|
+
`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// the block in AGENTS.md (other agents read it); CLAUDE.md imports AGENTS.md, or carries the block itself
|
|
101
|
+
const agents = join(root, 'AGENTS.md'), claude = join(root, 'CLAUDE.md');
|
|
102
|
+
put(agents, withBlock(existsSync(agents) ? readFileSync(agents, 'utf8') : ''));
|
|
103
|
+
if (!existsSync(claude)) put(claude, '@AGENTS.md\n');
|
|
104
|
+
else if (!/^@AGENTS\.md\s*$/m.test(readFileSync(claude, 'utf8'))) put(claude, withBlock(readFileSync(claude, 'utf8')));
|
|
105
|
+
|
|
106
|
+
// Claude Code: start the docs with every session, and tell the agent where they are (a SessionStart hook's output)
|
|
107
|
+
if (!noHook) {
|
|
108
|
+
const file = join(root, '.claude/settings.json'), settings = existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : {};
|
|
109
|
+
const command = 'npx --no-install lino docs --detach';
|
|
110
|
+
const starts = ((settings.hooks ??= {}).SessionStart ??= []);
|
|
111
|
+
if (!starts.some((g) => g.hooks?.some((h) => h.command === command))) {
|
|
112
|
+
starts.push({ hooks: [{ type: 'command', command }] });
|
|
113
|
+
put(file, JSON.stringify(settings, null, 2) + '\n');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
console.log(`\nDone. Agents read ${pkg}/AGENTS.md; the docs start with each Claude Code session (npx lino docs).`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------- icon: an app's own icons, cut like the kit's, into a module that registers them ----------
|
|
120
|
+
async function icons() {
|
|
121
|
+
const out = resolve(flag('--out', 'src/lib/icons.js')), files = args.filter((a) => a.endsWith('.svg'));
|
|
122
|
+
if (!files.length) die('usage: lino icon <file.svg…> [--out src/lib/icons.js]');
|
|
123
|
+
const { cutIcon, iconsModule, ROW } = await import(pathToFileURL(join(PKG, 'src/ds/icon-bake.js')).href);
|
|
124
|
+
// the icons already in the module stay, unless baked again now
|
|
125
|
+
const icons = {};
|
|
126
|
+
if (existsSync(out)) for (const m of readFileSync(out, 'utf8').matchAll(/^ {2}'?([\w-]+)'?: \{ viewBox: \[([^\]]*)\], ink: \[([^\]]*)\], d: '([^']*)' \},$/gm))
|
|
127
|
+
icons[m[1]] = { viewBox: m[2].split(', ').map(Number), ink: m[3].split(', ').map(Number), d: m[4] };
|
|
128
|
+
for (const file of files) {
|
|
129
|
+
const svg = readFileSync(file, 'utf8'), name = basename(file, '.svg');
|
|
130
|
+
// its own texture if the export carries one (Figma's TEXTURE effect), the kit's tools row's otherwise
|
|
131
|
+
const icon = icons[name] = cutIcon(svg, { textures: /feTurbulence/.test(svg) ? [] : [ROW] });
|
|
132
|
+
console.log(`${name}: ${icon.loops} loops, ${icon.d.length} chars`);
|
|
133
|
+
}
|
|
134
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
135
|
+
writeFileSync(out, iconsModule(icons, { name: 'ICONS', register: NAME, header: `// Baked by \`npx lino icon\` (lino-ui) from SVG vectors: do not edit, bake again. Import it once in the app's entry;
|
|
136
|
+
// then icon("<name>") draws each, cut by hand at the one icon weight.
|
|
137
|
+
` }));
|
|
138
|
+
console.log('wrote', relative(process.cwd(), out));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ---------- surface: n-slice surfaces (skill lino-texture) ----------
|
|
142
|
+
async function surface() {
|
|
143
|
+
const name = flag('--name', 'SURFACES'), [specPath, outPath] = args;
|
|
144
|
+
if (!specPath || !outPath) die('usage: lino surface <specs.mjs> <out.js> [--name SURFACES]');
|
|
145
|
+
const { bakeNSlice } = await import(pathToFileURL(join(PKG, 'src/ds/nslice.js')).href);
|
|
146
|
+
const specs = (await import(pathToFileURL(resolve(specPath)).href)).default, out = {};
|
|
147
|
+
for (const [key, spec] of Object.entries(specs)) {
|
|
148
|
+
const t0 = performance.now(), b = out[key] = bakeNSlice(spec);
|
|
149
|
+
console.log(`${key}: master ${b.w}×${b.h}, layers ${b.layers.length}, ${(JSON.stringify(b).length / 1024).toFixed(1)} KB, ${Math.round(performance.now() - t0)} ms`);
|
|
150
|
+
}
|
|
151
|
+
writeFileSync(outPath, `// Baked by \`npx lino surface\` (lino-ui) from ${relative(dirname(resolve(outPath)), resolve(specPath))}: do not edit, bake again.
|
|
152
|
+
// N-slice surfaces cut by hand: mountCut(el, ${name}.<name>) from "lino-ui/nslice" keeps one on an element at any size.
|
|
153
|
+
export const ${name} = ${JSON.stringify(out)};
|
|
154
|
+
`);
|
|
155
|
+
console.log('wrote', outPath);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const COMMANDS = { docs, init, icon: icons, surface };
|
|
159
|
+
if (!COMMANDS[cmd]) {
|
|
160
|
+
console.log(`lino-ui ${VERSION}
|
|
161
|
+
lino docs [--port 4977] [--detach] serve the documentation site; --detach leaves it running
|
|
162
|
+
lino init [--no-hook] give this project's agents the guide, the skills and the session hook
|
|
163
|
+
lino icon <svg…> [--out file.js] bake vectors into hand-cut icons for icon()
|
|
164
|
+
lino surface <specs.mjs> <out.js> bake n-slice surfaces
|
|
165
|
+
The guide: ${relative(process.cwd(), join(PKG, 'AGENTS.md'))}`);
|
|
166
|
+
process.exit(cmd && cmd !== 'help' && cmd !== '--help' ? 1 : 0);
|
|
167
|
+
}
|
|
168
|
+
await COMMANDS[cmd]();
|
package/docs/agents.md
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# Agents
|
|
2
|
+
|
|
3
|
+
How an agent builds interface with the kit: what to do, what never to do, when to cut a texture, which skill when. The package's AGENTS.md.
|
|
4
|
+
|
|
5
|
+
For agents (and people) who build screens with `lino-ui`: Svelte 5 components in lino's look, paper, ink, one
|
|
6
|
+
accent, every edge cut by hand. Read this file whole before the first piece of UI; it is short, and every rule in it
|
|
7
|
+
comes from review. Paths below are relative to the package (`node_modules/lino-ui/`).
|
|
8
|
+
|
|
9
|
+
## At the start of every session
|
|
10
|
+
|
|
11
|
+
1. Start the documentation site and tell the user where it is:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx lino docs --detach
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
It prints a local URL (the same one while it runs; it starts once). Say to the user, in their language, something
|
|
18
|
+
like: "The lino-ui docs are open at http://localhost:4977: every component with live examples, its props and
|
|
19
|
+
these rules." Do it again whenever you start the app's dev server, if it is not running.
|
|
20
|
+
2. Read this file. Before you use a component, read its page: `docs/<component>.md` (`docs/button.md`), or the same at
|
|
21
|
+
`<docs URL>/ds/<component>/llms.txt`. The index of every page is `docs/llms.txt`.
|
|
22
|
+
|
|
23
|
+
## Setting up an app
|
|
24
|
+
|
|
25
|
+
Svelte 5 with Vite (SvelteKit or plain). Once:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm i lino-ui
|
|
29
|
+
npx lino init
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`lino init` gives the project's agents this guide: the skills (`.claude/skills`, pointers into the package, so they
|
|
33
|
+
update with it), a block in `AGENTS.md` and `CLAUDE.md`, and a Claude Code hook that starts the docs with each session.
|
|
34
|
+
Then, in the app's entry (SvelteKit: the root `+layout.svelte`):
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
import "lino-ui/styles.css"; // tokens, fonts, the kit's look
|
|
38
|
+
import "lino-ui/setup"; // the rough cuts, the press, the interface sounds
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The page's background is the paper: `body { background: var(--color-background); color: var(--color-text); }`.
|
|
42
|
+
|
|
43
|
+
Three themes, only the colours change: Solar (light, the default), and the dark Ink and Lunar on
|
|
44
|
+
`<html data-theme="ink">` / `"lunar"`. Set the attribute before the first paint (a line of script in `<head>`), so a
|
|
45
|
+
dark theme never flashes paper. Everything you build follows by itself as long as it takes its colours from tokens.
|
|
46
|
+
|
|
47
|
+
## Building from a prompt
|
|
48
|
+
|
|
49
|
+
1. **Read the request as surfaces, controls, content.** Surfaces are what things sit on (a panel, a menu, a dialog,
|
|
50
|
+
a card). Controls are what the user acts on. Content is text, pictures, numbers. Icons belong to controls.
|
|
51
|
+
2. **Pick a component for every control and surface** (the table below). Compose; never restyle a component into
|
|
52
|
+
another one.
|
|
53
|
+
3. **Lay it out on the 4px grid.** Spaces are `--space-1` 4px … `--space-8` 32px. Hug the content, the same padding
|
|
54
|
+
on every side, one height and one centre line for everything on a bar.
|
|
55
|
+
4. **One surface per level.** A control on a plate uses that plate; it never brings its own fill inside it. Group by
|
|
56
|
+
space first; a separator only between real groups.
|
|
57
|
+
5. **Check the list of don'ts**, then look at it in the browser (see Verify).
|
|
58
|
+
|
|
59
|
+
### Which component
|
|
60
|
+
|
|
61
|
+
| The user needs | Use |
|
|
62
|
+
|---|---|
|
|
63
|
+
| An action | `Button`: `variant="action"` on a surface, `"panel"` straight on the paper, `"accent"` for the one primary action of a view, `"ghost"` for quiet ones |
|
|
64
|
+
| An action shown as an icon | `Button size="icon"` (`icon-sm`, `icon-lg`) with `aria-label`, inside a `Tooltip` with its name and shortcut |
|
|
65
|
+
| Several actions joined | `ButtonGroup` |
|
|
66
|
+
| On / off | `Switch` with a `Label` |
|
|
67
|
+
| One of a few, all visible | `ToggleGroup` (a track); a pressed state on one button: `Toggle` |
|
|
68
|
+
| A set of tools | `Toolbar` holding a `ToggleGroup variant="bare" size="icon"` |
|
|
69
|
+
| One of many, or actions behind a button | `DropdownMenu` (radio items for a choice, `.lino-grid` for tiles) |
|
|
70
|
+
| An app's menus | `Menubar` |
|
|
71
|
+
| Text in | `Input` with a `Label` |
|
|
72
|
+
| A number in a range | `Slider` |
|
|
73
|
+
| A decision that stops the flow | `Dialog` (title, description, footer with the actions) |
|
|
74
|
+
| A shortcut | `Kbd keys="mod shift z"` |
|
|
75
|
+
| Something is loading | `Spinner` with `aria-label` |
|
|
76
|
+
| A rank or tier | `Tag`, a framed `Picture`, a `Plaque` |
|
|
77
|
+
| A break between groups | `Separator` (rarely) |
|
|
78
|
+
| A panel, card or section of your own | a `div` with `{@attach cut('plate')}` and `--surface-bg` (Texture, below) |
|
|
79
|
+
|
|
80
|
+
```svelte
|
|
81
|
+
<script>
|
|
82
|
+
import { Button } from "lino-ui/button";
|
|
83
|
+
import * as Tooltip from "lino-ui/tooltip";
|
|
84
|
+
import { icon } from "lino-ui";
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
<Tooltip.Provider>
|
|
88
|
+
<Tooltip.Root>
|
|
89
|
+
<Tooltip.Trigger>
|
|
90
|
+
{#snippet child({ props })}
|
|
91
|
+
<Button {...props} size="icon" aria-label="Search">{@html icon("search")}</Button>
|
|
92
|
+
{/snippet}
|
|
93
|
+
</Tooltip.Trigger>
|
|
94
|
+
<Tooltip.Content sideOffset={8}>Search</Tooltip.Content>
|
|
95
|
+
</Tooltip.Root>
|
|
96
|
+
</Tooltip.Provider>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Do
|
|
100
|
+
|
|
101
|
+
- Use the kit's component whenever there is one, as its page shows it. Its props are the API; `class` adds layout
|
|
102
|
+
(margins, grid placement), never a new look.
|
|
103
|
+
- Colours, type, space and motion are tokens (`src/ds/tokens.css`): `--color-background` (paper), `--color-text`
|
|
104
|
+
(ink), `--color-surface` (plates), `--color-accent` (on, primary, focus), `--color-muted` (captions, shortcuts),
|
|
105
|
+
`--color-border`, `--color-divider`. A hover, a pressed state or a tint is `color-mix()` of tokens where it is used:
|
|
106
|
+
`color-mix(in srgb, var(--color-text) 6%, var(--color-surface))`.
|
|
107
|
+
- The chrome speaks Alegreya Sans SC (`--font-display`): buttons, menus, labels, titles. Inter (`--font-text`) only for
|
|
108
|
+
running text. Sizes `--text-xs` 14 … `--text-2xl` 56px.
|
|
109
|
+
- Accent orange means "on" or "primary": one accent action per view, a switch that is on, the focus ring.
|
|
110
|
+
- Every white surface on the paper gets the border: `--surface-border: var(--color-border)`.
|
|
111
|
+
- Name every icon-only control (`aria-label`) and show the name, with the shortcut, in a tooltip.
|
|
112
|
+
- Keep the keyboard: every control is reachable, focus rings stay whole (`outline: var(--focus-ring)`).
|
|
113
|
+
- Motion: short fades and 4px slides with `--dur` and `--ease-out`; a press shrinks by `--press` (the kit does it).
|
|
114
|
+
|
|
115
|
+
## Don't
|
|
116
|
+
|
|
117
|
+
- **No CSS `border`, `border-radius`, `box-shadow` or `filter`.** Not on anything. No pills, no rounded corners, no
|
|
118
|
+
circles in rectangles, no elevation. A surface separates from the paper by its fill and a cut border.
|
|
119
|
+
- No hex colours and no new colour tokens: `hsl()` tokens and `color-mix()` of them. A colour written as a value
|
|
120
|
+
(`white`, `#fff`, `hsl(0 0% 100%)`) breaks the dark themes; `--color-surface` is white only in Solar.
|
|
121
|
+
- No translucent colour on an icon: its stroke darkens where it overlaps. Use an opaque token (`--color-muted`).
|
|
122
|
+
- No inline SVG icons, icon fonts, lucide or emoji as icons. Icons are `icon(name)`; a new one is baked (Icons).
|
|
123
|
+
- No decoration: no glyph that means nothing, no divider for looks, no control the screen doesn't need now.
|
|
124
|
+
- Hover never moves anything: one tint step, a quiet label to full ink. No scale, no translate on hover.
|
|
125
|
+
- Layout never lives only on an open state (`[data-state=open]`): what fades out would jump.
|
|
126
|
+
- Never copy a mockup's frames, badges, numerals or icons as pictures. From a mockup take only photos and
|
|
127
|
+
illustrations; everything else is built with the kit.
|
|
128
|
+
- Never outline text. Text is real text.
|
|
129
|
+
|
|
130
|
+
## Texture: when to cut, and with what
|
|
131
|
+
|
|
132
|
+
Every surface and border in lino is cut, like paper trimmed with scissors. A cut is a vector outline displaced by
|
|
133
|
+
noise: no filter, no image of an edge. Decide by what the thing is:
|
|
134
|
+
|
|
135
|
+
| It is | Cut? | How |
|
|
136
|
+
|---|---|---|
|
|
137
|
+
| A kit component (button, menu, dialog, toolbar, tag…) | Already cut | Nothing. Don't add a border or a fill to it |
|
|
138
|
+
| A panel, card, section, popover, list box or code block you compose, 32px or more | Yes | `{@attach cut('plate')}` from `lino-ui`; set `--surface-bg`, and `--surface-border` for a border |
|
|
139
|
+
| A strip, chip or field of your own, 16–32px high | Yes | `{@attach cut('chip')}` |
|
|
140
|
+
| A label with a thick border | Yes | `{@attach cut('tag')}`, or the kit's `Tag` |
|
|
141
|
+
| A badge, frame, medal, tile, ticket, any shape that is not a plain rectangle, a piece of art | Yes | Skill **lino-texture**: a fixed shape is cut with `texturize` (or baked); one that resizes is an n-slice surface |
|
|
142
|
+
| A new icon | Yes, baked | `npx lino icon <file.svg>` (Icons) |
|
|
143
|
+
| A photo or an illustration | Not the picture | Clip it with a cut shape (`clip-path`), the picture untouched; or the kit's `Picture` |
|
|
144
|
+
| Text | Never | Real text on top of the cut |
|
|
145
|
+
| A layout wrapper with no fill (a row, a grid, a column) | No | Nothing to cut: it has no edge |
|
|
146
|
+
| A plate inside a plate | No | One surface per level: drop the inner fill, group by space |
|
|
147
|
+
| The page's paper | No | `--color-background` on `body` |
|
|
148
|
+
| A divider | Already cut | `Separator` (a cut strip, `--rough-rule`) |
|
|
149
|
+
| A focus ring | No | `outline: var(--focus-ring)`, never clipped (the fill sits on a layer behind the content) |
|
|
150
|
+
|
|
151
|
+
A plate of your own:
|
|
152
|
+
|
|
153
|
+
```svelte
|
|
154
|
+
<script>
|
|
155
|
+
import { cut } from "lino-ui";
|
|
156
|
+
</script>
|
|
157
|
+
|
|
158
|
+
<section class="card" {@attach cut("plate")}>…</section>
|
|
159
|
+
|
|
160
|
+
<style>
|
|
161
|
+
.card { --surface-bg: var(--color-surface); --surface-border: var(--color-border); padding: var(--space-6); }
|
|
162
|
+
</style>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Neighbouring plates get different seeds by themselves. A state changes `--surface-bg` / `--surface-border` only; the
|
|
166
|
+
outline never changes.
|
|
167
|
+
|
|
168
|
+
## Icons
|
|
169
|
+
|
|
170
|
+
`icon(name)` from `lino-ui` returns the `<svg>` markup: `{@html icon("search")}`. It is coloured by `currentColor`,
|
|
171
|
+
sized by its glyph, and in the kit's buttons, toggles and menu rows it is sized and weighted for you: beside a label
|
|
172
|
+
`.8em`, 12px from it, as heavy as the label's letters; alone 20, 24 or 26px on a 32, 40 or 56px control. `ICON_NAMES()`
|
|
173
|
+
lists the set.
|
|
174
|
+
|
|
175
|
+
A new icon is never drawn in code. Get the vector (a Figma export with strokes, not outlined) and bake it:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
npx lino icon design/icons/*.svg --out src/lib/icons.js
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
It cuts every glyph at the one icon weight with the hand-cut texture. Import the output once in the app's entry
|
|
182
|
+
(`import "./lib/icons.js"`): it registers the icons, and `icon("<file name>")` draws them. Skill **lino-ui**, Icons.
|
|
183
|
+
|
|
184
|
+
## Skills: which one when
|
|
185
|
+
|
|
186
|
+
| Skill | Use it for |
|
|
187
|
+
|---|---|
|
|
188
|
+
| **lino-ui** | Any interface work with the kit: a screen, a panel, a form, a menu, a dialog, choosing a component, an icon, "in lino's style" |
|
|
189
|
+
| **lino-texture** | A shape the kit doesn't have: a badge, a frame, a plaque, a tile, a decorative plate, art, a surface that stretches (n-slice) |
|
|
190
|
+
|
|
191
|
+
`npx lino init` installs them for Claude Code (`.claude/skills`). Any other agent reads them from
|
|
192
|
+
`skills/<name>/SKILL.md`.
|
|
193
|
+
|
|
194
|
+
## Verify
|
|
195
|
+
|
|
196
|
+
Look at it in the browser before you call it done: a zoomed screenshot of every edge (cut, never straight, no seams)
|
|
197
|
+
and of the spacing; measure paddings on the cut edge, not the box; tab through it with the keyboard; check that every
|
|
198
|
+
icon-only control has a name. Then tell the user what to look at, and that the docs are at the URL `lino docs` gave.
|
|
199
|
+
|
|
200
|
+
## The CLI
|
|
201
|
+
|
|
202
|
+
| Command | Does |
|
|
203
|
+
|---|---|
|
|
204
|
+
| `npx lino docs [--port 4977] [--detach]` | Serves the documentation site; `--detach` leaves it running and prints its URL |
|
|
205
|
+
| `npx lino init` | Gives the project's agents this guide: skills, the AGENTS.md / CLAUDE.md block, the session hook |
|
|
206
|
+
| `npx lino icon <svg…> [--out file.js]` | Bakes SVG vectors into hand-cut icons for `icon()` |
|
|
207
|
+
| `npx lino surface <specs.mjs> <out.js>` | Bakes n-slice surfaces (skill lino-texture) |
|
|
208
|
+
|
|
209
|
+
## Where things are
|
|
210
|
+
|
|
211
|
+
| Path | What |
|
|
212
|
+
|---|---|
|
|
213
|
+
| `docs/` | Every page of the site as Markdown: `llms.txt` (the index), `llms-full.txt` (all), `<page>.md` |
|
|
214
|
+
| `skills/` | The agent skills |
|
|
215
|
+
| `src/ds/PRINCIPLES.md` | The full rules, with their reasons |
|
|
216
|
+
| `src/lib/components/ui/` | The components' source |
|
|
217
|
+
| `src/lib/lino.css`, `src/ds/tokens.css` | The look and the tokens |
|
|
218
|
+
| `site/` | The documentation site that `lino docs` serves |
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
## This site as Markdown
|
|
222
|
+
|
|
223
|
+
Every page of this site is also Markdown for language models, after the llms.txt convention: **Copy Markdown** on each
|
|
224
|
+
page, its file `/ds/<page>/llms.txt`, the index `llms.txt`, everything in one file `llms-full.txt`. In the package
|
|
225
|
+
the same files are in `docs/`.
|
|
226
|
+
|
|
227
|
+
## The skills' files
|
|
228
|
+
|
|
229
|
+
- **lino-texture** (`../skills/lino-texture/SKILL.md`): Cut new shapes by hand for lino-ui: badges, frames, plaques, tiles, tickets, medals, decorative plates, art, and surfaces that stretch to any size (n-slice / 9-slice / 3-slice), with lino's vector texture (texturize, bakeNSlice, mountCut from lino-ui).
|
|
230
|
+
- **lino-ui** (`../skills/lino-ui/SKILL.md`): Build interface with lino-ui, the Svelte 5 kit in lino's hand-cut look (paper, ink, one accent, every edge cut by hand).
|