@ruckc/truss-ui 1.0.0 → 1.1.1

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/README.md CHANGED
@@ -87,6 +87,18 @@ scripts/ API-reference and token extraction for the docs
87
87
 
88
88
  The API reference is **generated from the source** (`npm run docs`), so it can't drift from the code. The test suite is described in [Testing](https://truss.ruck.io/docs/testing/).
89
89
 
90
+ ## Releases
91
+
92
+ Every push to `main` that passes CI is released automatically: the version is worked out from the commit messages ([Conventional Commits](https://www.conventionalcommits.org)), all four packages are published to npm at that same version, and a GitHub release and `CHANGELOG.md` entry are created.
93
+
94
+ | Commit | Release |
95
+ | --- | --- |
96
+ | `feat: …` | minor |
97
+ | `fix:`, `perf:`, `revert:`, `docs:`, `refactor:`, `test:`, `build:`, `ci:`, `chore:`, `style:` | patch |
98
+ | any type with `!` (e.g. `feat!: …`) or a `BREAKING CHANGE:` footer | major |
99
+
100
+ Commits that don't follow the format (no `type:` prefix) don't trigger a release on their own. Publishing uses npm [trusted publishing](https://docs.npmjs.com/trusted-publishers) (OIDC from the `ci.yml` workflow), so no npm token is stored anywhere. Each package's trusted publisher on npmjs.com must be for workflow `ci.yml` with the **`npm publish`** action allowed (the default is only `npm stage publish`, which npm rejects with `403 OIDC permission denied for this action`).
101
+
90
102
  ## Licence
91
103
 
92
104
  MIT © Curtis Ruck. The bundled Inter and JetBrains Mono fonts are under the SIL Open Font License 1.1.
@@ -0,0 +1,8 @@
1
+ <script lang="ts">
2
+ import { dialogs } from '../dialogs.svelte.js';
3
+ import DialogRequest from './internal/DialogRequest.svelte';
4
+
5
+ const req = $derived(dialogs.current);
6
+ </script>
7
+
8
+ {#if req}{#key req.id}<DialogRequest {req} />{/key}{/if}
@@ -0,0 +1,3 @@
1
+ declare const DialogHost: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type DialogHost = ReturnType<typeof DialogHost>;
3
+ export default DialogHost;
@@ -0,0 +1,99 @@
1
+ <script lang="ts">
2
+ import Button from '../Button.svelte';
3
+ import Dialog from '../Dialog.svelte';
4
+ import Input from '../Input.svelte';
5
+ import Textarea from '../Textarea.svelte';
6
+ import Icon from '../../icons/Icon.svelte';
7
+ import { dialogs, type DialogRequest } from '../../dialogs.svelte.js';
8
+
9
+ /** One on-screen dialog for a queued `dialogs.alert/confirm/prompt` call. Used by DialogHost. */
10
+ let { req }: { req: DialogRequest } = $props();
11
+ // the host remounts this component per request (keyed), so reading `req` once is intended
12
+
13
+ const glyph = { primary: 'info', warning: 'warning', danger: 'error' } as const;
14
+ const formId = $props.id();
15
+
16
+ let open = $state(true);
17
+ // svelte-ignore state_referenced_locally
18
+ let value = $state(req.defaultValue ?? '');
19
+ let error = $state<string>();
20
+ let body = $state<HTMLElement>();
21
+ let chosen = false;
22
+ let result: unknown;
23
+
24
+ function done(v: unknown) {
25
+ chosen = true;
26
+ result = v;
27
+ open = false; // close natively first so focus returns to the opener; `onclose` then settles
28
+ }
29
+ function submit(e?: Event) {
30
+ e?.preventDefault();
31
+ if (req.kind === 'prompt') {
32
+ const msg = req.validate?.(value);
33
+ if (msg) { error = msg; return; }
34
+ done(value);
35
+ } else done(req.kind === 'confirm' ? true : undefined);
36
+ }
37
+ function closed() {
38
+ dialogs.settle(req.id, chosen ? result : req.kind === 'confirm' ? false : req.kind === 'prompt' ? null : undefined);
39
+ }
40
+
41
+ // Native dialogs put you straight into the answer: the field, the OK button, or Cancel for destructive confirmations.
42
+ $effect(() => {
43
+ const target = body?.closest('dialog')?.querySelector<HTMLElement>('[data-autofocus]');
44
+ target?.focus();
45
+ if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) target.select();
46
+ });
47
+ </script>
48
+
49
+ <Dialog bind:open title={req.title} size="sm" alert={req.kind !== 'prompt'} onclose={closed}>
50
+ <div bind:this={body} class="row" data-intent={req.intent}>
51
+ <span class="ic"><Icon name={glyph[req.intent]} size={20} /></span>
52
+ <form id={formId} onsubmit={submit}>
53
+ <p class="msg">{req.message}</p>
54
+ {#if req.kind === 'prompt'}
55
+ {#if req.multiline}
56
+ <Textarea
57
+ bind:value
58
+ label={req.label}
59
+ {error}
60
+ placeholder={req.placeholder}
61
+ data-autofocus
62
+ oninput={() => (error = undefined)}
63
+ onkeydown={(e) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) submit(e); }}
64
+ />
65
+ {:else}
66
+ <Input
67
+ bind:value
68
+ label={req.label}
69
+ {error}
70
+ type={req.type ?? 'text'}
71
+ placeholder={req.placeholder}
72
+ data-autofocus
73
+ oninput={() => (error = undefined)}
74
+ />
75
+ {/if}
76
+ {/if}
77
+ </form>
78
+ </div>
79
+ {#snippet footer()}
80
+ {#if req.kind !== 'alert'}
81
+ <Button variant="minimal" intent="neutral" data-autofocus={req.intent === 'danger' && req.kind === 'confirm' ? '' : undefined} onclick={() => done(req.kind === 'confirm' ? false : null)}>{req.cancelLabel}</Button>
82
+ {/if}
83
+ <Button
84
+ type="submit"
85
+ form={formId}
86
+ intent={req.intent}
87
+ data-autofocus={req.kind === 'alert' || (req.kind === 'confirm' && req.intent !== 'danger') ? '' : undefined}
88
+ >{req.okLabel}</Button>
89
+ {/snippet}
90
+ </Dialog>
91
+
92
+ <style>
93
+ .row { display: flex; gap: 12px; line-height: 1.45; }
94
+ .row form { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 12px; }
95
+ .msg { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; padding-top: 5px; }
96
+ .ic { flex: none; display: grid; place-items: center; width: 32px; height: 32px; border-radius: 50%; background: var(--tr-primary-container); color: var(--tr-on-primary-container); }
97
+ .row[data-intent='warning'] .ic { background: var(--tr-warning-container); color: var(--tr-on-warning-container); }
98
+ .row[data-intent='danger'] .ic { background: var(--tr-danger-container); color: var(--tr-on-danger-container); }
99
+ </style>
@@ -0,0 +1,7 @@
1
+ import { type DialogRequest } from '../../dialogs.svelte.js';
2
+ type $$ComponentProps = {
3
+ req: DialogRequest;
4
+ };
5
+ declare const DialogRequest: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type DialogRequest = ReturnType<typeof DialogRequest>;
7
+ export default DialogRequest;
@@ -0,0 +1,63 @@
1
+ export type DialogIntent = 'primary' | 'warning' | 'danger';
2
+ export type DialogKind = 'alert' | 'confirm' | 'prompt';
3
+ type Base = {
4
+ /** Heading shown in the dialog header. Defaults to "Notice", "Confirm" or "Input". */
5
+ title?: string;
6
+ intent?: DialogIntent;
7
+ /** Label of the confirming button. Defaults to "OK". */
8
+ okLabel?: string;
9
+ };
10
+ export type AlertOptions = Base;
11
+ export type ConfirmOptions = Base & {
12
+ /** Label of the dismissing button. Defaults to "Cancel". */
13
+ cancelLabel?: string;
14
+ };
15
+ export type PromptOptions = ConfirmOptions & {
16
+ /** Label above the field. */
17
+ label?: string;
18
+ defaultValue?: string;
19
+ placeholder?: string;
20
+ type?: 'text' | 'password' | 'email' | 'url' | 'number' | 'tel';
21
+ /** Use a multi-line field (Ctrl/Cmd+Enter submits). */
22
+ multiline?: boolean;
23
+ /** Return an error message to keep the dialog open, or nothing when the value is acceptable. */
24
+ validate?: (value: string) => string | null | undefined | false | void;
25
+ };
26
+ export type DialogRequest = {
27
+ id: number;
28
+ kind: DialogKind;
29
+ message: string;
30
+ title: string;
31
+ intent: DialogIntent;
32
+ okLabel: string;
33
+ cancelLabel: string;
34
+ label?: string;
35
+ defaultValue?: string;
36
+ placeholder?: string;
37
+ type?: PromptOptions['type'];
38
+ multiline?: boolean;
39
+ validate?: PromptOptions['validate'];
40
+ resolve: (value: any) => void;
41
+ };
42
+ /**
43
+ * Promise-based replacements for the browser's `alert()`, `confirm()` and `prompt()`.
44
+ * Requests are shown one at a time, in order, by a single `<DialogHost />`.
45
+ */
46
+ declare class DialogStore {
47
+ #private;
48
+ queue: DialogRequest[];
49
+ /** The request currently on screen. */
50
+ get current(): DialogRequest | undefined;
51
+ /** Show a message; resolves when it is acknowledged (or dismissed). */
52
+ alert(message: string, opts?: AlertOptions): Promise<void>;
53
+ /** Ask a yes/no question; resolves `true` on OK and `false` on Cancel, Escape or backdrop click. */
54
+ confirm(message: string, opts?: ConfirmOptions): Promise<boolean>;
55
+ /** Ask for text; resolves the entered string on OK and `null` on Cancel, Escape or backdrop click. */
56
+ prompt(message: string, opts?: PromptOptions): Promise<string | null>;
57
+ /** Resolve a request and move on to the next one. Used by `<DialogHost />`; safe to call twice. */
58
+ settle(id: number, value: unknown): void;
59
+ /** Dismiss every pending dialog as if cancelled. */
60
+ clear(): void;
61
+ }
62
+ export declare const dialogs: DialogStore;
63
+ export {};
@@ -0,0 +1,54 @@
1
+ const titles = { alert: 'Notice', confirm: 'Confirm', prompt: 'Input' };
2
+ /**
3
+ * Promise-based replacements for the browser's `alert()`, `confirm()` and `prompt()`.
4
+ * Requests are shown one at a time, in order, by a single `<DialogHost />`.
5
+ */
6
+ class DialogStore {
7
+ queue = $state([]);
8
+ #next = 1;
9
+ /** The request currently on screen. */
10
+ get current() {
11
+ return this.queue[0];
12
+ }
13
+ #ask(kind, message, o) {
14
+ return new Promise((resolve) => {
15
+ this.queue.push({
16
+ ...o,
17
+ id: this.#next++,
18
+ kind,
19
+ message,
20
+ title: o.title ?? titles[kind],
21
+ intent: o.intent ?? 'primary',
22
+ okLabel: o.okLabel ?? 'OK',
23
+ cancelLabel: o.cancelLabel ?? 'Cancel',
24
+ resolve
25
+ });
26
+ });
27
+ }
28
+ /** Show a message; resolves when it is acknowledged (or dismissed). */
29
+ alert(message, opts = {}) {
30
+ return this.#ask('alert', message, opts);
31
+ }
32
+ /** Ask a yes/no question; resolves `true` on OK and `false` on Cancel, Escape or backdrop click. */
33
+ confirm(message, opts = {}) {
34
+ return this.#ask('confirm', message, opts);
35
+ }
36
+ /** Ask for text; resolves the entered string on OK and `null` on Cancel, Escape or backdrop click. */
37
+ prompt(message, opts = {}) {
38
+ return this.#ask('prompt', message, opts);
39
+ }
40
+ /** Resolve a request and move on to the next one. Used by `<DialogHost />`; safe to call twice. */
41
+ settle(id, value) {
42
+ const r = this.queue.find((q) => q.id === id);
43
+ if (!r)
44
+ return;
45
+ this.queue = this.queue.filter((q) => q.id !== id);
46
+ r.resolve(value);
47
+ }
48
+ /** Dismiss every pending dialog as if cancelled. */
49
+ clear() {
50
+ for (const r of [...this.queue])
51
+ this.settle(r.id, r.kind === 'confirm' ? false : r.kind === 'prompt' ? null : undefined);
52
+ }
53
+ }
54
+ export const dialogs = new DialogStore();
package/dist/index.d.ts CHANGED
@@ -59,11 +59,14 @@ export { default as Badge } from './components/Badge.svelte';
59
59
  export { default as DataTable } from './components/DataTable.svelte';
60
60
  export { default as Dialog } from './components/Dialog.svelte';
61
61
  export { default as Toaster } from './components/Toaster.svelte';
62
+ export { default as DialogHost } from './components/DialogHost.svelte';
62
63
  export { default as Icon } from './icons/Icon.svelte';
63
64
  export { toasts } from './toast.svelte.js';
65
+ export { dialogs } from './dialogs.svelte.js';
64
66
  export type { IconName } from './icons/paths.js';
65
67
  export type { ColumnFilters, SortState, TreeNode, UploadItem, MenuEntry, Option, DateRange, Column, MenuItem, Intent } from './types.js';
66
68
  export type { Toast, ToastIntent } from './toast.svelte.js';
69
+ export type { AlertOptions, ConfirmOptions, PromptOptions, DialogIntent, DialogKind } from './dialogs.svelte.js';
67
70
  export { parseDuration, formatDuration } from './duration.js';
68
71
  export type { DurationUnit } from './duration.js';
69
72
  export { parseHex, toHex, contrast } from './color.js';
package/dist/index.js CHANGED
@@ -59,7 +59,9 @@ export { default as Badge } from './components/Badge.svelte';
59
59
  export { default as DataTable } from './components/DataTable.svelte';
60
60
  export { default as Dialog } from './components/Dialog.svelte';
61
61
  export { default as Toaster } from './components/Toaster.svelte';
62
+ export { default as DialogHost } from './components/DialogHost.svelte';
62
63
  export { default as Icon } from './icons/Icon.svelte';
63
64
  export { toasts } from './toast.svelte.js';
65
+ export { dialogs } from './dialogs.svelte.js';
64
66
  export { parseDuration, formatDuration } from './duration.js';
65
67
  export { parseHex, toHex, contrast } from './color.js';
package/package.json CHANGED
@@ -1,77 +1,89 @@
1
1
  {
2
- "name": "@ruckc/truss-ui",
3
- "version": "1.0.0",
4
- "description": "Svelte 5 / SvelteKit design system blending Material 3 surfaces with Blueprint density",
5
- "keywords": [
6
- "svelte",
7
- "sveltekit",
8
- "design-system",
9
- "ui",
10
- "components",
11
- "accessible",
12
- "material",
13
- "blueprint"
14
- ],
15
- "type": "module",
16
- "license": "MIT",
17
- "author": "Curtis Ruck",
18
- "homepage": "https://truss.ruck.io",
19
- "repository": {
20
- "type": "git",
21
- "url": "git+https://github.com/ruckc/truss-ui.git"
22
- },
23
- "bugs": {
24
- "url": "https://github.com/ruckc/truss-ui/issues"
25
- },
26
- "publishConfig": {
27
- "access": "public"
28
- },
29
- "scripts": {
30
- "dev": "vite dev",
31
- "docs": "node scripts/extract-api.mjs",
32
- "build": "npm run docs && vite build",
33
- "preview": "vite preview",
34
- "package": "svelte-kit sync && svelte-package",
35
- "package:all": "npm run package && npm run package --workspaces",
36
- "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
37
- "test": "playwright test",
38
- "test:quick": "playwright test tests/lib.spec.ts tests/inputs.spec.ts tests/misc.spec.ts"
39
- },
40
- "files": [
41
- "dist",
42
- "!dist/**/*.test.*",
43
- "!dist/**/*.spec.*"
44
- ],
45
- "sideEffects": [
46
- "**/*.css"
47
- ],
48
- "svelte": "./dist/index.js",
49
- "types": "./dist/index.d.ts",
50
- "exports": {
51
- ".": {
52
- "types": "./dist/index.d.ts",
53
- "svelte": "./dist/index.js"
54
- },
55
- "./styles.css": "./dist/styles/index.css",
56
- "./tokens.css": "./dist/styles/tokens.css",
57
- "./fonts.css": "./dist/styles/fonts.css"
58
- },
59
- "peerDependencies": {
60
- "svelte": "^5.20.0"
61
- },
62
- "devDependencies": {
63
- "@playwright/test": "^1.65.0",
64
- "@sveltejs/adapter-static": "^4.0.1",
65
- "@sveltejs/kit": "^2.20.0",
66
- "@sveltejs/package": "^2.3.0",
67
- "@sveltejs/vite-plugin-svelte": "^6.0.0",
68
- "@types/node": "^26.6.6",
69
- "svelte": "^5.20.0",
70
- "svelte-check": "^4.0.0",
71
- "typescript": "^5.6.0",
72
- "vite": "^7.0.0"
73
- },
74
- "workspaces": [
75
- "packages/*"
76
- ]
2
+ "author": "Curtis Ruck",
3
+ "bugs": {
4
+ "url": "https://github.com/ruckc/truss-ui/issues"
5
+ },
6
+ "description": "Svelte 5 / SvelteKit design system blending Material 3 surfaces with Blueprint density",
7
+ "devDependencies": {
8
+ "@playwright/test": "^1.65.0",
9
+ "@semantic-release/changelog": "^7.0.0",
10
+ "@semantic-release/exec": "^7.1.0",
11
+ "@semantic-release/git": "^11.0.1",
12
+ "@sveltejs/adapter-static": "^4.0.1",
13
+ "@sveltejs/kit": "^3.0.0",
14
+ "@sveltejs/package": "^3.0.0",
15
+ "@sveltejs/vite-plugin-svelte": "^7.0.0",
16
+ "@types/node": "^26.6.6",
17
+ "conventional-changelog-conventionalcommits": "^8.0.0",
18
+ "semantic-release": "^25.0.9",
19
+ "svelte": "^5.57.2",
20
+ "svelte-check": "^4.7.5",
21
+ "typescript": "^6.0.0",
22
+ "vite": "^8.0.12"
23
+ },
24
+ "engines": {
25
+ "node": ">=22.17"
26
+ },
27
+ "exports": {
28
+ ".": {
29
+ "svelte": "./dist/index.js",
30
+ "types": "./dist/index.d.ts"
31
+ },
32
+ "./fonts.css": "./dist/styles/fonts.css",
33
+ "./styles.css": "./dist/styles/index.css",
34
+ "./tokens.css": "./dist/styles/tokens.css"
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "!dist/**/*.test.*",
39
+ "!dist/**/*.spec.*"
40
+ ],
41
+ "homepage": "https://truss.ruck.io",
42
+ "imports": {
43
+ "#lib": "./src/lib/index.ts",
44
+ "#lib/*": "./src/lib/*"
45
+ },
46
+ "keywords": [
47
+ "svelte",
48
+ "sveltekit",
49
+ "design-system",
50
+ "ui",
51
+ "components",
52
+ "accessible",
53
+ "material",
54
+ "blueprint"
55
+ ],
56
+ "license": "MIT",
57
+ "name": "@ruckc/truss-ui",
58
+ "peerDependencies": {
59
+ "svelte": "^5.20.0"
60
+ },
61
+ "publishConfig": {
62
+ "access": "public"
63
+ },
64
+ "repository": {
65
+ "type": "git",
66
+ "url": "git+https://github.com/ruckc/truss-ui.git"
67
+ },
68
+ "scripts": {
69
+ "build": "npm run docs && vite build",
70
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
71
+ "dev": "vite dev",
72
+ "docs": "node scripts/extract-api.mjs",
73
+ "package": "svelte-kit sync && svelte-package",
74
+ "package:all": "npm run package && npm run package --workspaces",
75
+ "preview": "vite preview",
76
+ "test": "playwright test",
77
+ "test:quick": "playwright test tests/lib.spec.ts tests/inputs.spec.ts tests/misc.spec.ts"
78
+ },
79
+ "sideEffects": [
80
+ "**/*.css"
81
+ ],
82
+ "svelte": "./dist/index.js",
83
+ "type": "module",
84
+ "types": "./dist/index.d.ts",
85
+ "version": "1.1.1",
86
+ "workspaces": [
87
+ "packages/*"
88
+ ]
77
89
  }