@sakaniui/react 0.8.12 → 0.9.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 +9 -3
- package/CHANGELOG.md +37 -0
- package/README.md +22 -1
- package/bin/sakaniui-mcp.mjs +65 -10
- package/dist/components/Sonner/Sonner.d.ts +30 -0
- package/dist/components/Sonner/index.d.ts +4 -0
- package/dist/components/Sonner/state.d.ts +106 -0
- package/dist/icon-index.json +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +1493 -1101
- package/dist/manifest.json +142 -5
- package/dist/style.css +1 -1
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -78,7 +78,7 @@ import { Button, Card } from '@sakaniui/react';
|
|
|
78
78
|
(`aria-label`, or `label` on `socialLinks`). Status changes use live regions (`Alert`, `Toast`,
|
|
79
79
|
`Spinner`, `EmptyState` are `role="status"`/`alert`). Never remove focus rings.
|
|
80
80
|
7. **Don't fake what the system doesn't do.** `Toast` is presentational — you own queueing and
|
|
81
|
-
positioning. `Menu` is a surface — you own anchoring (pair it with `Popover`). Dragging is a
|
|
81
|
+
positioning (for a ready-made stack, use `Toaster` and `toast()`). `Menu` is a surface — you own anchoring (pair it with `Popover`). Dragging is a
|
|
82
82
|
look, not a behaviour. Check "Behaviour you must supply" in the table below before assuming.
|
|
83
83
|
8. **Compose in slots, don't fork.** Components expose slots (`leading`, `trailing`, `meta`,
|
|
84
84
|
`assignees`, `icon`, `actions`). Fill a slot with other system components rather than copying a
|
|
@@ -153,7 +153,7 @@ CSS of a component into your own file.
|
|
|
153
153
|
Format: **Use for** · **Why it is built this way** · **Not for / instead**.
|
|
154
154
|
|
|
155
155
|
**Looking for a component by another name?** SakaniUI uses its own names. A *dialog* or *alert dialog* is
|
|
156
|
-
`Modal`; a *snackbar* is `Toast
|
|
156
|
+
`Modal`; a *snackbar* is `Toast` (or `Toaster` + `toast()` for a managed stack); a *dropdown menu* is `Menu`; a *navbar* or *app bar* is `TopBar`; a
|
|
157
157
|
*date picker* is `Calendar`; a *loader* is `Spinner`; a *toggle* is `Switch`; a *banner* is `Announcement`;
|
|
158
158
|
*autocomplete* is `Combobox`. There is no `Dialog`, `AlertDialog`, `Snackbar` or `Navbar`. Each component
|
|
159
159
|
lists its other names under `aliases` in `manifest.json`, and `search_components` understands them.
|
|
@@ -174,6 +174,11 @@ lists its other names under `aliases` in `manifest.json`, and `search_components
|
|
|
174
174
|
- **Toast** — Use for short-lived confirmation or error after an action. *Why:* presentational only;
|
|
175
175
|
queueing and positioning are the app's job, so it fits any notification system. *Not for* anything the user
|
|
176
176
|
must act on — a toast disappears.
|
|
177
|
+
- **Toaster + toast()** — Use for notifications you trigger from code: mount one `<Toaster />`, call
|
|
178
|
+
`toast.success('Saved')` from anywhere. *Why:* it owns queueing, position (6), the 4s timer, the pile that
|
|
179
|
+
spreads on hover and swipe-to-dismiss, so the app does not. `toast.promise` turns one toast from loading
|
|
180
|
+
into success or error. *Not for* anything the user must act on — a toast disappears; a plain static card
|
|
181
|
+
→ `Toast`. Needs `Toaster` mounted once, and `toast()` is a function, not a component.
|
|
177
182
|
- **Badge** — Use for a short status, category or count. *Why:* 6 colours × subtle/solid, no sizes, no
|
|
178
183
|
borders — it is a label, not a control. *Not for* clickable filters → `FilterChip`.
|
|
179
184
|
- **Tooltip** — Use for brief supplementary text on hover/focus. *Why:* pure CSS, so it works with
|
|
@@ -261,7 +266,8 @@ and **tell the user when you are composing** rather than implying a dedicated co
|
|
|
261
266
|
|
|
262
267
|
| Component | You own |
|
|
263
268
|
|---|---|
|
|
264
|
-
| Toast | queueing, positioning, auto-dismiss timing |
|
|
269
|
+
| Toast | queueing, positioning, auto-dismiss timing (or use `Toaster`) |
|
|
270
|
+
| Toaster | mounting it once; it handles the rest |
|
|
265
271
|
| Menu | anchoring/positioning (pair with Popover) |
|
|
266
272
|
| BoardCard `dragging` | drag-and-drop logic and drop targets |
|
|
267
273
|
| BoardColumn `loading` | the skeleton cards inside it |
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@sakaniui/react` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.9.0
|
|
6
|
+
|
|
7
|
+
- **New: `Toaster` and `toast()`**, a managed toast stack (Sonner-style) built from the new *↳ Sonner* page in Figma.
|
|
8
|
+
Mount one `<Toaster />`, then call `toast()`, `toast.success()`, `.info()`, `.warning()`, `.error()`,
|
|
9
|
+
`.loading()`, `.promise()` and `.dismiss()` from anywhere.
|
|
10
|
+
- New toasts rise in from the edge; older ones pile up behind the newest (lifted 14px, shrunk 5% per level,
|
|
11
|
+
content hidden, three visible). Hover or focus spreads the pile into a list and pauses the 4s timers;
|
|
12
|
+
leaving folds it back. Swipe a toast towards its edge to dismiss it (past 45px, or a quick flick, it flies
|
|
13
|
+
out in 200ms; otherwise it springs back in 400ms). Motion is 400ms `ease`.
|
|
14
|
+
- Options per toast: `description`, `icon`, `action`, `cancel`, `closeButton`, `duration`, `dismissible`,
|
|
15
|
+
`richColors`, `id` (update in place), `onDismiss`, `onAutoClose`. On `Toaster`: `position` (six),
|
|
16
|
+
`richColors`, `closeButton`, `expand`, `visibleToasts`, `duration`, `gap`, `offset`, `mobileOffset`,
|
|
17
|
+
`hotkey` (alt+T focuses the stack; Escape folds it).
|
|
18
|
+
- Light, dark and glass surfaces through the existing tokens; reduced motion keeps the fades only; errors
|
|
19
|
+
are announced assertively.
|
|
20
|
+
- The existing `Toast` is unchanged: it stays the static, presentational card.
|
|
21
|
+
- Agent contract: `Toaster` is in the manifest with recipes (show a toast, a promise toast) and the aliases
|
|
22
|
+
*sonner*, *toast stack*, *toast provider*. (`toaster` now means this component, not `Toast`.)
|
|
23
|
+
|
|
24
|
+
## 0.8.13
|
|
25
|
+
|
|
26
|
+
A user reported the MCP server "didn't work". Reproduced: on a Node older than 16.9 the server connected
|
|
27
|
+
and listed its tools, then **every tool call failed** with `Object.hasOwn is not a function`.
|
|
28
|
+
|
|
29
|
+
- **Fixed: tool calls failing on older Node.** The server no longer uses `Object.hasOwn` (Node 16.9+) or the
|
|
30
|
+
`node:` import prefix, so it works on Node 16 (tested on 16.20) as well as 18, 20, 22 and 26. Node older
|
|
31
|
+
than 16 now stops with a plain message ("Node.js 14.18.0 is too old... needs Node.js 18 or later") instead
|
|
32
|
+
of failing later. 18+ remains the supported range.
|
|
33
|
+
- **New: `npx -y @sakaniui/mcp --doctor`** prints a report (Node version, platform, whether `npx` is on the
|
|
34
|
+
PATH, the reference data, the version found in your project) and exercises three real tools. Also
|
|
35
|
+
`--version` and `--help`.
|
|
36
|
+
- **The build now guards against this**: it fails if the server uses an API newer than Node 16, and it runs a
|
|
37
|
+
real tool call with `Object.hasOwn` removed from the runtime.
|
|
38
|
+
- **Troubleshooting** added to the README, the package README and sakaniui.com/docs/ai (PATH problems in
|
|
39
|
+
desktop editors, Windows, proxies, stale `npx` caches).
|
|
40
|
+
- Correction: the 0.8.12 notes said "250+" aliases; the number is 249.
|
|
41
|
+
|
|
5
42
|
## 0.8.12
|
|
6
43
|
|
|
7
44
|
Found while testing 0.8.11 in Claude Code: after writing correct code, the agent offered to wrap the
|
package/README.md
CHANGED
|
@@ -155,6 +155,27 @@ Tools: `search_components`, `get_component` (with a type-checked `example`), `se
|
|
|
155
155
|
an export that is not described, a prop that is not declared, an example that does not type-check, an
|
|
156
156
|
icon name that does not exist, a token that is not defined, or anything that looks like a private reference.
|
|
157
157
|
|
|
158
|
+
### Troubleshooting the MCP server
|
|
159
|
+
|
|
160
|
+
Run this in a terminal first. It checks Node, the data, the tools and your project, and prints a report you can
|
|
161
|
+
paste when asking for help:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
npx -y @sakaniui/mcp --doctor
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
| Symptom | Fix |
|
|
168
|
+
|---|---|
|
|
169
|
+
| `spawn npx ENOENT`, or the editor cannot start it | Desktop editors often start with a smaller `PATH` than your terminal (common with nvm and Homebrew). Use the full path: run `which npx` (macOS/Linux) or `where npx` (Windows) and put that in `"command"`, e.g. `"/opt/homebrew/bin/npx"`. |
|
|
170
|
+
| Windows | `npx` is a `.cmd` script: use `"command": "cmd"` with `"args": ["/c", "npx", "-y", "@sakaniui/mcp"]`. |
|
|
171
|
+
| Connects, but tool calls fail (before 0.8.13) | Node older than 16.9. Update Node (18 or later is supported), or use `@sakaniui/mcp@latest`. |
|
|
172
|
+
| "Node.js ... is too old" | Install Node 18 or later: https://nodejs.org (check with `node --version`). |
|
|
173
|
+
| Times out on the first start | The first run downloads about 200 kB. Run `npx -y @sakaniui/mcp --version` once in a terminal to warm the cache, then restart the editor. |
|
|
174
|
+
| Behind a company proxy or private npm registry | The machine must reach `registry.npmjs.org` (check `npm config get registry`) and `@sakaniui/*` packages must be allowed. |
|
|
175
|
+
| Odd `npx` errors, or an old version keeps running | Delete the `_npx` folder inside your npm cache (find it with `npm config get cache`), then restart the editor. |
|
|
176
|
+
| Added it but no tools appear | Restart the editor. In Claude Code run `/mcp` and look for `sakaniui` with 10 tools. |
|
|
177
|
+
| `Failed to connect` after typing the command by hand | Check the package name for a full-width `@` or stray characters; copy the command from here instead. |
|
|
178
|
+
|
|
158
179
|
## Glass & liquid glass
|
|
159
180
|
|
|
160
181
|
Glass is built into the theme system, not added component by component. Next to light/dark, SakaniUI has a second axis — **Surface** — with three modes: **Solid** (the default), **Glass** (frosted translucency) and **Liquid** (Apple-style liquid glass: the backdrop *bends* at the edges like a lens, with a faint color fringe, rim light and depth). Components read a small set of `--surface-*` tokens, so switching an area from Solid to Glass or Liquid re-themes everything inside it at once — in the Figma file (variable modes and effect styles) and in code. Solid stays pixel-identical to before.
|
|
@@ -241,7 +262,7 @@ Change a semantic token and every component follows — in both the design file
|
|
|
241
262
|
|
|
242
263
|
**Forms** — Input · Textarea · Select · Checkbox · Radio · Switch · Slider · Combobox (single/multi, async loading) · File Upload
|
|
243
264
|
|
|
244
|
-
**Composite** — Card · Alert · Toast · Accordion · Tabs · Breadcrumb · Table (generic, selectable) · Stat Card (sparklines) · Stepper · Calendar (single + range, dropdown navigation) · Pagination · Popover · Segmented Control · List Item
|
|
265
|
+
**Composite** — Card · Alert · Toast · Toaster (Sonner-style `toast()` stack) · Accordion · Tabs · Breadcrumb · Table (generic, selectable) · Stat Card (sparklines) · Stepper · Calendar (single + range, dropdown navigation) · Pagination · Popover · Segmented Control · List Item
|
|
245
266
|
|
|
246
267
|
**Sidebar kit** — Sidebar · Header · Search · Item · Sub Item · Group Label · Divider · Promo · Footer — nine standalone parts that compose into full navigation
|
|
247
268
|
|
package/bin/sakaniui-mcp.mjs
CHANGED
|
@@ -17,10 +17,21 @@
|
|
|
17
17
|
* manifest era, answers come from this copy but every component is checked against its `since`
|
|
18
18
|
* release and flagged when the project does not have it yet.
|
|
19
19
|
*/
|
|
20
|
-
import fs from '
|
|
21
|
-
import path from '
|
|
22
|
-
import readline from '
|
|
23
|
-
import { fileURLToPath } from '
|
|
20
|
+
import fs from 'fs';
|
|
21
|
+
import path from 'path';
|
|
22
|
+
import readline from 'readline';
|
|
23
|
+
import { fileURLToPath } from 'url';
|
|
24
|
+
|
|
25
|
+
/* ---------- Node version guard ---------- */
|
|
26
|
+
// Tested on Node 16, 18, 20, 22 and 26; 18+ is the supported range. Say so plainly instead of failing later in a way
|
|
27
|
+
// that looks like the server is broken (before 0.8.13 a Node older than 16.9 connected, listed its tools, then failed every call).
|
|
28
|
+
const NODE_MAJOR = Number(process.versions.node.split('.')[0]);
|
|
29
|
+
if (NODE_MAJOR < 16) {
|
|
30
|
+
process.stderr.write(`sakaniui-mcp: Node.js ${process.versions.node} is too old. SakaniUI's MCP server needs Node.js 18 or later (https://nodejs.org). Run \`node --version\` to check.\n`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
// `Object.hasOwn` is Node 16.9+; this works everywhere
|
|
34
|
+
const has = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
|
|
24
35
|
|
|
25
36
|
const ownRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
37
|
|
|
@@ -48,6 +59,21 @@ function findProjectInstall() {
|
|
|
48
59
|
}
|
|
49
60
|
|
|
50
61
|
// inside @sakaniui/react the data sits next to bin/; inside @sakaniui/mcp it is copied into data/
|
|
62
|
+
/* ---------- command line: --version, --help, --doctor ---------- */
|
|
63
|
+
const argvFlag = (...names) => process.argv.slice(2).some((a) => names.includes(a));
|
|
64
|
+
if (argvFlag('--help', '-h')) {
|
|
65
|
+
process.stdout.write(`sakaniui-mcp: the SakaniUI MCP server (read-only). Your editor starts it; there is nothing to type.
|
|
66
|
+
|
|
67
|
+
npx -y @sakaniui/mcp start the server (waits silently for an editor)
|
|
68
|
+
npx -y @sakaniui/mcp --doctor check this machine and print a report (paste it when asking for help)
|
|
69
|
+
npx -y @sakaniui/mcp --version print the version
|
|
70
|
+
npx -y @sakaniui/mcp --project <dir> answer for the @sakaniui/react installed in <dir>
|
|
71
|
+
|
|
72
|
+
Docs: https://www.sakaniui.com/docs/ai
|
|
73
|
+
`);
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
|
|
51
77
|
const bundled = loadInstall(fs.existsSync(path.join(ownRoot, 'dist', 'manifest.json')) ? ownRoot : path.join(ownRoot, 'data'));
|
|
52
78
|
if (!bundled || !bundled.manifest) {
|
|
53
79
|
process.stderr.write('sakaniui-mcp: dist/manifest.json not found. Run `npm run build:lib` (or install a published version).\n');
|
|
@@ -442,8 +468,10 @@ function checkCode(source) {
|
|
|
442
468
|
if (imp.def) errors.push({ line: imp.line, message: `${entry} has no default export; use a named import: import { ${imp.def} } from '${entry}';` });
|
|
443
469
|
for (const n of imp.names) {
|
|
444
470
|
if (n.typeOnly) continue;
|
|
445
|
-
const
|
|
446
|
-
const isUtility =
|
|
471
|
+
const util = manifest.utilities?.find((u) => u.name === n.imported);
|
|
472
|
+
const isUtility = !!util;
|
|
473
|
+
// an exact helper name (toast) is not a mis-cased component (Toast)
|
|
474
|
+
const c = isUtility ? undefined : byName.get(n.imported.toLowerCase());
|
|
447
475
|
if (entry === '@sakaniui/react/icons') {
|
|
448
476
|
if (!iconNames.has(n.imported) && !iconOtherExports.has(n.imported)) {
|
|
449
477
|
const hint = searchIcons(n.imported, 3).map((i) => i.name);
|
|
@@ -463,6 +491,7 @@ function checkCode(source) {
|
|
|
463
491
|
errors.push({ line: imp.line, message: `${n.imported} is not exported by @sakaniui/react.${aliasHint(n.imported)}${near.length ? ` Did you mean: ${near.join(', ')}?` : ''} Use search_components.` });
|
|
464
492
|
}
|
|
465
493
|
if (c && inInstalled(c) === false) errors.push({ line: imp.line, message: `${c.name} was added in ${c.since}; the project has ${projectVersion}.` });
|
|
494
|
+
if (util && inInstalled(util) === false) errors.push({ line: imp.line, message: `${util.name} was added in ${util.since}; the project has ${projectVersion}.` });
|
|
466
495
|
}
|
|
467
496
|
}
|
|
468
497
|
|
|
@@ -666,7 +695,7 @@ const tools = {
|
|
|
666
695
|
const g = strArg(args, 'group'); const q = strArg(args, 'query');
|
|
667
696
|
const e = firstError(g, q); if (e) return fail(e.err);
|
|
668
697
|
const groups = manifest.tokens.groups;
|
|
669
|
-
if (g.value && !
|
|
698
|
+
if (g.value && !has(groups, g.value)) return fail(`Unknown token group "${g.value}". Groups: ${Object.keys(groups).join(', ')}`);
|
|
670
699
|
const needle = q.value?.toLowerCase();
|
|
671
700
|
const out = {};
|
|
672
701
|
for (const [name, toks] of Object.entries(groups)) {
|
|
@@ -691,7 +720,7 @@ const tools = {
|
|
|
691
720
|
const errors = [];
|
|
692
721
|
const warnings = [];
|
|
693
722
|
if (inInstalled(c) === false) errors.push(`${c.name} was added in ${c.since}; the project has ${projectVersion}. It does not exist there.`);
|
|
694
|
-
for (const r of c.contract.requiredProps) if (!
|
|
723
|
+
for (const r of c.contract.requiredProps) if (!has(props, r)) errors.push(`Missing required prop "${r}".`);
|
|
695
724
|
for (const [k, v] of Object.entries(props)) {
|
|
696
725
|
const p = known.get(k);
|
|
697
726
|
if (!p) { const u = unknownProp(c, k); if (u) (u.level === 'error' ? errors : warnings).push(u.message); continue; }
|
|
@@ -809,7 +838,7 @@ function handle(msg) {
|
|
|
809
838
|
return reply(id, { result: {
|
|
810
839
|
protocolVersion: asked && PROTOCOL_VERSIONS.includes(asked) ? asked : PROTOCOL_VERSIONS[0],
|
|
811
840
|
capabilities: { tools: { listChanged: false } },
|
|
812
|
-
serverInfo: { name: 'sakaniui', title: 'SakaniUI', version:
|
|
841
|
+
serverInfo: { name: 'sakaniui', title: 'SakaniUI', version: bundled.version }, // the server's own version, not the data's
|
|
813
842
|
instructions: `SakaniUI ${refVersion}${projectVersion ? ` (project has ${projectVersion})` : ''}. Read-only reference. Look components up with search_components / get_component and start from their \`example\`; find icon names with search_icons (never guess them); search by the name you would normally use (dialog, snackbar, navbar, date picker) and it maps to the real component; run check_code on every snippet before you answer; style anything beyond props with the design tokens from get_tokens (CSS variables), not hard-coded colours or utility classes the project may not have; read get_guide when nothing fits.${note ? ` ${note}` : ''}`,
|
|
814
843
|
} });
|
|
815
844
|
}
|
|
@@ -818,7 +847,7 @@ function handle(msg) {
|
|
|
818
847
|
case 'tools/list':
|
|
819
848
|
return reply(id, { result: { tools: Object.entries(tools).map(([name, t]) => ({ name, description: t.description, inputSchema: t.inputSchema, annotations: READ_ONLY })) } });
|
|
820
849
|
case 'tools/call': {
|
|
821
|
-
const t = typeof params?.name === 'string' &&
|
|
850
|
+
const t = typeof params?.name === 'string' && has(tools, params.name) ? tools[params.name] : null;
|
|
822
851
|
if (!t) return reply(id, { error: { code: -32602, message: `Unknown tool: ${String(params?.name).slice(0, 60)}` } });
|
|
823
852
|
const args = params.arguments;
|
|
824
853
|
if (args !== undefined && args !== null && (typeof args !== 'object' || Array.isArray(args))) return reply(id, { result: fail('Tool arguments must be an object.') });
|
|
@@ -837,6 +866,32 @@ function handle(msg) {
|
|
|
837
866
|
}
|
|
838
867
|
}
|
|
839
868
|
|
|
869
|
+
/* ---------- --version and --doctor (no protocol traffic; print and exit) ---------- */
|
|
870
|
+
if (argvFlag('--version', '-v')) { process.stdout.write(`${bundled.version}\n`); process.exit(0); }
|
|
871
|
+
if (argvFlag('--doctor')) {
|
|
872
|
+
const lines = [];
|
|
873
|
+
const row = (label, value, ok) => lines.push(` ${ok === false ? '✘' : ok === 'warn' ? '!' : ok === true ? '✔' : ' '} ${label.padEnd(20)}${value}`);
|
|
874
|
+
row('server version', bundled.version, true);
|
|
875
|
+
row('Node.js', NODE_MAJOR >= 18 ? `${process.version}` : `${process.version} (18+ is supported; 16 works but is end-of-life, please update)`, NODE_MAJOR >= 18 ? true : 'warn');
|
|
876
|
+
row('platform', `${process.platform} ${process.arch}`);
|
|
877
|
+
row('working folder', process.cwd());
|
|
878
|
+
row('reference data', `${manifest.components.length} components and blocks, ${Object.values(manifest.tokens.groups).reduce((a, g) => a + Object.keys(g).length, 0)} tokens, ${iconIndex.length} icons, describing @sakaniui/react ${refVersion} (${answeringFromProject ? "taken from your project's install" : 'bundled with this package'})`, manifest.components.length > 100 && iconIndex.length > 1000);
|
|
879
|
+
row('your project', project ? `@sakaniui/react ${project.version} at ${project.root}` : 'no @sakaniui/react found from this folder (fine for a quick check; run it from your project folder, or pass --project <dir>, to answer for your version)');
|
|
880
|
+
const exe = process.platform === 'win32' ? ['npx.cmd', 'npx.exe', 'npx'] : ['npx'];
|
|
881
|
+
const onPath = (process.env.PATH || '').split(path.delimiter).find((d) => d && exe.some((e) => fs.existsSync(path.join(d, e))));
|
|
882
|
+
row('npx on PATH', onPath ? onPath : 'not found on this PATH', !!onPath);
|
|
883
|
+
// exercise the real tools, the same code paths an editor calls
|
|
884
|
+
const probe = (name, args) => { try { const r = tools[name].run(args); return r.isError ? { ok: false, text: r.content[0].text } : { ok: true, text: r.content[0].text }; } catch (e) { return { ok: false, text: String(e && e.message || e) }; } };
|
|
885
|
+
const s1 = probe('search_components', { query: 'dialog' });
|
|
886
|
+
row('tool: search', s1.ok && /Modal/.test(s1.text) ? 'search_components("dialog") finds Modal' : `FAILED: ${s1.text.slice(0, 120)}`, s1.ok && /Modal/.test(s1.text));
|
|
887
|
+
const s2 = probe('check_code', { code: "import { Button } from '@sakaniui/react';\n<Button variant=\"danger\">x</Button>" });
|
|
888
|
+
row('tool: check_code', s2.ok && /not allowed/.test(s2.text) ? 'catches a wrong prop value' : `FAILED: ${s2.text.slice(0, 120)}`, s2.ok && /not allowed/.test(s2.text));
|
|
889
|
+
const s3 = probe('get_tokens', { group: 'radius' });
|
|
890
|
+
row('tool: get_tokens', s3.ok ? 'ok' : `FAILED: ${s3.text.slice(0, 120)}`, s3.ok);
|
|
891
|
+
process.stdout.write(`SakaniUI MCP doctor\n${lines.join('\n')}\n\nIf your editor still cannot start it: the editor may use a different PATH than this terminal\n(common with nvm and with desktop apps; on Windows use "cmd" with args ["/c","npx","-y","@sakaniui/mcp"]).\nPaste this report when asking for help: https://github.com/samzydd/Sakaniui/issues\n`);
|
|
892
|
+
process.exit(lines.some((l) => l.includes('✘')) ? 1 : 0);
|
|
893
|
+
}
|
|
894
|
+
|
|
840
895
|
const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
841
896
|
rl.on('line', (line) => {
|
|
842
897
|
if (!line.trim()) return;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
export type ToastPosition = 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
|
|
3
|
+
export interface ToasterProps {
|
|
4
|
+
/** Corner or edge the stack sits on. Defaults to "bottom-right". */
|
|
5
|
+
position?: ToastPosition;
|
|
6
|
+
/** Tints success, info, warning and error toasts by type. */
|
|
7
|
+
richColors?: boolean;
|
|
8
|
+
/** Shows the round close button on every toast. */
|
|
9
|
+
closeButton?: boolean;
|
|
10
|
+
/** Keeps the stack spread out instead of piled up. */
|
|
11
|
+
expand?: boolean;
|
|
12
|
+
/** How many toasts show at once; older ones fade out. Defaults to 3. */
|
|
13
|
+
visibleToasts?: number;
|
|
14
|
+
/** Milliseconds before a toast closes itself. Defaults to 4000. */
|
|
15
|
+
duration?: number;
|
|
16
|
+
/** Space between toasts when spread out, in px. Defaults to 14. */
|
|
17
|
+
gap?: number;
|
|
18
|
+
/** Distance from the viewport edge, in px or any CSS length. Defaults to 24. */
|
|
19
|
+
offset?: number | string;
|
|
20
|
+
/** Distance from the viewport edge on screens 600px wide or less. Defaults to 16. */
|
|
21
|
+
mobileOffset?: number | string;
|
|
22
|
+
/** Keys that focus the stack. Defaults to alt+T (`['altKey', 'KeyT']`). */
|
|
23
|
+
hotkey?: string[];
|
|
24
|
+
/** Accessible name of the notifications region. The hotkey is appended. Defaults to "Notifications". */
|
|
25
|
+
containerAriaLabel?: string;
|
|
26
|
+
className?: string;
|
|
27
|
+
style?: React.CSSProperties;
|
|
28
|
+
}
|
|
29
|
+
export declare const Toaster: React.FC<ToasterProps>;
|
|
30
|
+
export default Toaster;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
export type ToastType = 'default' | 'success' | 'info' | 'warning' | 'error' | 'loading';
|
|
3
|
+
export type ToastId = string | number;
|
|
4
|
+
export interface ToastAction {
|
|
5
|
+
/** Button text. */
|
|
6
|
+
label: React.ReactNode;
|
|
7
|
+
/** Called on click. The toast closes afterwards unless you call `event.preventDefault()`. */
|
|
8
|
+
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
|
9
|
+
}
|
|
10
|
+
export interface ToastCancel {
|
|
11
|
+
/** Button text. */
|
|
12
|
+
label: React.ReactNode;
|
|
13
|
+
/** Called on click, before the toast closes. */
|
|
14
|
+
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
|
15
|
+
}
|
|
16
|
+
export interface ToastOptions {
|
|
17
|
+
/** Reuse an id to update a toast in place instead of adding a new one. */
|
|
18
|
+
id?: ToastId;
|
|
19
|
+
/** Secondary line under the title. */
|
|
20
|
+
description?: React.ReactNode;
|
|
21
|
+
/** Replaces the type icon. Pass `null` to show no icon. */
|
|
22
|
+
icon?: React.ReactNode;
|
|
23
|
+
/** Primary button, right-most. */
|
|
24
|
+
action?: ToastAction;
|
|
25
|
+
/** Secondary button, left of the action. */
|
|
26
|
+
cancel?: ToastCancel;
|
|
27
|
+
/** Milliseconds before the toast closes itself. Overrides the Toaster's `duration`. `Infinity` keeps it open. */
|
|
28
|
+
duration?: number;
|
|
29
|
+
/** Shows the round close button on this toast. Overrides the Toaster's `closeButton`. */
|
|
30
|
+
closeButton?: boolean;
|
|
31
|
+
/** When false the toast cannot be swiped away or closed by the user. Defaults to true. */
|
|
32
|
+
dismissible?: boolean;
|
|
33
|
+
/** Announces the toast assertively (interrupting the screen reader). Errors are announced this way by default. */
|
|
34
|
+
important?: boolean;
|
|
35
|
+
/** Tints this toast by its type. Overrides the Toaster's `richColors`. */
|
|
36
|
+
richColors?: boolean;
|
|
37
|
+
/** Called when the toast is closed by the user (close button, cancel, swipe) or by `toast.dismiss()`. */
|
|
38
|
+
onDismiss?: (toast: ToastData) => void;
|
|
39
|
+
/** Called when the toast closes itself after its duration. */
|
|
40
|
+
onAutoClose?: (toast: ToastData) => void;
|
|
41
|
+
/** Extra class on the toast element. */
|
|
42
|
+
className?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface ToastData extends ToastOptions {
|
|
45
|
+
id: ToastId;
|
|
46
|
+
type: ToastType;
|
|
47
|
+
title: React.ReactNode;
|
|
48
|
+
/** @internal set by toast.dismiss(); the Toaster plays the exit motion, then removes it */
|
|
49
|
+
delete?: boolean;
|
|
50
|
+
/** @internal bumped on every update so the timer restarts */
|
|
51
|
+
version: number;
|
|
52
|
+
}
|
|
53
|
+
export interface ToastPromiseMessages<T> extends Omit<ToastOptions, 'id'> {
|
|
54
|
+
id?: ToastId;
|
|
55
|
+
/** Shown while the promise is pending. Leave it out to show nothing until it settles. */
|
|
56
|
+
loading?: React.ReactNode;
|
|
57
|
+
/** Shown when it resolves. A function receives the resolved value. */
|
|
58
|
+
success?: React.ReactNode | ((data: T) => React.ReactNode);
|
|
59
|
+
/** Shown when it rejects. A function receives the error. */
|
|
60
|
+
error?: React.ReactNode | ((error: unknown) => React.ReactNode);
|
|
61
|
+
/** Called after it settles either way. */
|
|
62
|
+
finally?: () => void;
|
|
63
|
+
}
|
|
64
|
+
type Event = {
|
|
65
|
+
kind: 'upsert';
|
|
66
|
+
toast: ToastData;
|
|
67
|
+
} | {
|
|
68
|
+
kind: 'dismiss';
|
|
69
|
+
id?: ToastId;
|
|
70
|
+
};
|
|
71
|
+
type Listener = (event: Event) => void;
|
|
72
|
+
/** @internal */
|
|
73
|
+
export declare const store: {
|
|
74
|
+
subscribe(listener: Listener): () => void;
|
|
75
|
+
/** Toasts that are open right now, newest first. */
|
|
76
|
+
active: () => ToastData[];
|
|
77
|
+
/** @internal the Toaster calls this once a toast has finished its exit motion */
|
|
78
|
+
remove(id: ToastId): void;
|
|
79
|
+
};
|
|
80
|
+
declare function dismiss(id?: ToastId): ToastId | undefined;
|
|
81
|
+
declare function promise<T>(input: Promise<T> | (() => Promise<T>), messages: ToastPromiseMessages<T>): ToastId;
|
|
82
|
+
type ToastFn = (title: React.ReactNode, options?: ToastOptions) => ToastId;
|
|
83
|
+
/**
|
|
84
|
+
* Shows a toast. Needs one `<Toaster />` mounted somewhere in the app.
|
|
85
|
+
*
|
|
86
|
+
* toast('Event has been created', { description: 'Monday, January 3rd at 6:00pm' })
|
|
87
|
+
* toast.success('Saved')
|
|
88
|
+
* toast.error('Upload failed', { action: { label: 'Retry', onClick: retry } })
|
|
89
|
+
* toast.promise(save(), { loading: 'Saving…', success: 'Saved', error: 'Could not save' })
|
|
90
|
+
* toast.dismiss(id)
|
|
91
|
+
*
|
|
92
|
+
* Every call returns the toast's id; pass it back as `id` to update that toast in place.
|
|
93
|
+
*/
|
|
94
|
+
export declare const toast: ToastFn & {
|
|
95
|
+
message: ToastFn;
|
|
96
|
+
success: ToastFn;
|
|
97
|
+
info: ToastFn;
|
|
98
|
+
warning: ToastFn;
|
|
99
|
+
error: ToastFn;
|
|
100
|
+
/** Stays open (no timer) until you update it with the same `id` or dismiss it. */
|
|
101
|
+
loading: ToastFn;
|
|
102
|
+
promise: typeof promise;
|
|
103
|
+
/** Closes one toast, or every toast when called without an id. */
|
|
104
|
+
dismiss: typeof dismiss;
|
|
105
|
+
};
|
|
106
|
+
export {};
|