terminoir 0.14.0 → 0.15.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/README.md CHANGED
@@ -159,6 +159,7 @@ Building blocks so apps compose screens without CSS of their own. See the demo's
159
159
  | `PropertyRow` | Compact icon + label row for inline-edited values (record properties); hover highlights the row |
160
160
  | `ChipSelect`, `DateField` | Value shown as a coloured `Chip` that opens a grouped option list (`ChipSelectOption`, `ChipSelectGroup`); date shown as "Today" / "Tomorrow" / weekday / "Nov 12" (`dayLabel`), `deadline` tints past and today, opens the native picker, Backspace clears |
161
161
  | `TitleInput`, `FileButton`, `RadioCards` | Borderless heading input; button that opens a file picker; card-style radio group |
162
+ | `EditableText` | A name you click to rename in place (Enter or click away saves, Escape cancels); `PageHeader` and `ListItem` take `onrename` for the same |
162
163
  | `CalendarGrid` | Week (scrolling hours) and month grids; `CalendarEvent` with `muted`/`color`; helpers `calendarRange`, `shiftCalendar`, `eventsOn` |
163
164
  | `CanvasStage`, `DocumentViewer` | Full-bleed canvas host with floating overlay and side panel; paged viewer with toolbar and highlight marks |
164
165
  | `AccentSwatches` | `bind:value` accent hex from `ACCENT_PALETTE` |
@@ -0,0 +1,122 @@
1
+ <script lang="ts">
2
+ import { tick } from 'svelte';
3
+ import { cn } from '../utils/cn.js';
4
+ import { renamedValue } from '../utils/rename.js';
5
+
6
+ let {
7
+ value,
8
+ label,
9
+ onrename,
10
+ class: className = '',
11
+ }: {
12
+ /** The current name, shown as text until clicked. */
13
+ value: string;
14
+ /** Accessible name of the field while editing, such as "Dashboard name". */
15
+ label: string;
16
+ /** Called with the trimmed new name; not called for a blank or unchanged name. */
17
+ onrename: (value: string) => void | Promise<void>;
18
+ class?: string;
19
+ } = $props();
20
+
21
+ let editing = $state(false);
22
+ let draft = $state('');
23
+ /** The name being saved, shown until `onrename` settles. */
24
+ let saving = $state<string | null>(null);
25
+ let trigger = $state<HTMLButtonElement>();
26
+
27
+ function focusAndSelect(node: HTMLInputElement) {
28
+ node.focus();
29
+ node.select();
30
+ }
31
+
32
+ function start() {
33
+ draft = value;
34
+ editing = true;
35
+ }
36
+
37
+ async function stop(refocus: boolean) {
38
+ editing = false;
39
+ if (refocus) {
40
+ await tick();
41
+ trigger?.focus();
42
+ }
43
+ }
44
+
45
+ async function commit(refocus: boolean) {
46
+ if (!editing) return;
47
+ const next = renamedValue(draft, value);
48
+ void stop(refocus);
49
+ if (next === null) return;
50
+ saving = next;
51
+ try {
52
+ await onrename(next);
53
+ } finally {
54
+ saving = null;
55
+ }
56
+ }
57
+
58
+ function onkeydown(event: KeyboardEvent) {
59
+ if (event.key === 'Enter') {
60
+ event.preventDefault();
61
+ void commit(true);
62
+ } else if (event.key === 'Escape') {
63
+ event.preventDefault();
64
+ event.stopPropagation();
65
+ void stop(true);
66
+ }
67
+ }
68
+ </script>
69
+
70
+ {#if editing}
71
+ <input
72
+ class={cn('terminoir-editable-text terminoir-editable-text--input', className)}
73
+ aria-label={label}
74
+ autocomplete="off"
75
+ bind:value={draft}
76
+ use:focusAndSelect
77
+ {onkeydown}
78
+ onblur={() => void commit(false)}
79
+ />
80
+ {:else}
81
+ <button
82
+ bind:this={trigger}
83
+ type="button"
84
+ class={cn('terminoir-editable-text', className)}
85
+ title="Click to rename"
86
+ onclick={start}
87
+ >{saving ?? value}</button>
88
+ {/if}
89
+
90
+ <style>
91
+ .terminoir-editable-text {
92
+ display: inline-block;
93
+ max-width: 100%;
94
+ margin: 0 -0.25rem;
95
+ padding: 0 0.25rem;
96
+ border: 1px solid transparent;
97
+ border-radius: var(--radius-control);
98
+ background: transparent;
99
+ color: inherit;
100
+ font: inherit;
101
+ letter-spacing: inherit;
102
+ line-height: inherit;
103
+ text-align: start;
104
+ vertical-align: bottom;
105
+ box-sizing: content-box;
106
+ }
107
+ button.terminoir-editable-text {
108
+ overflow: hidden;
109
+ text-overflow: ellipsis;
110
+ white-space: inherit;
111
+ overflow-wrap: inherit;
112
+ cursor: text;
113
+ }
114
+ button.terminoir-editable-text:hover { border-color: color-mix(in srgb, var(--foreground) 10%, transparent); }
115
+ /* The ring is the border, so it is not clipped inside truncating rows. */
116
+ .terminoir-editable-text:focus-visible { outline: none; box-shadow: none; border-color: var(--ring); }
117
+ .terminoir-editable-text--input {
118
+ min-width: 6ch;
119
+ field-sizing: content;
120
+ border-color: var(--ring);
121
+ }
122
+ </style>
@@ -0,0 +1,12 @@
1
+ type $$ComponentProps = {
2
+ /** The current name, shown as text until clicked. */
3
+ value: string;
4
+ /** Accessible name of the field while editing, such as "Dashboard name". */
5
+ label: string;
6
+ /** Called with the trimmed new name; not called for a blank or unchanged name. */
7
+ onrename: (value: string) => void | Promise<void>;
8
+ class?: string;
9
+ };
10
+ declare const EditableText: import("svelte").Component<$$ComponentProps, {}, "">;
11
+ type EditableText = ReturnType<typeof EditableText>;
12
+ export default EditableText;
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
3
  import { cn } from '../utils/cn.js';
4
+ import EditableText from './EditableText.svelte';
4
5
 
5
6
  let {
6
7
  title,
@@ -13,6 +14,8 @@
13
14
  expanded,
14
15
  details,
15
16
  children,
17
+ onrename,
18
+ renameLabel = 'Name',
16
19
  ...rest
17
20
  }: {
18
21
  title?: string;
@@ -30,6 +33,10 @@
30
33
  details?: Snippet;
31
34
  /** Replaces title/description for custom content. */
32
35
  children?: Snippet;
36
+ /** Makes the title editable in place (rows without `onclick` only). */
37
+ onrename?: (title: string) => void | Promise<void>;
38
+ /** Accessible name of the title field while renaming. */
39
+ renameLabel?: string;
33
40
  } & Record<string, unknown> = $props();
34
41
  </script>
35
42
 
@@ -39,7 +46,11 @@
39
46
  {#if children}
40
47
  {@render children()}
41
48
  {:else}
42
- {#if title}<span class="terminoir-list-item__title">{title}</span>{/if}
49
+ {#if title}
50
+ <span class={cn('terminoir-list-item__title', onrename && !onclick && 'terminoir-list-item__title--editable')}>
51
+ {#if onrename && !onclick}<EditableText value={title} label={renameLabel} {onrename} />{:else}{title}{/if}
52
+ </span>
53
+ {/if}
43
54
  {#if description}<span class="terminoir-list-item__description">{description}</span>{/if}
44
55
  {/if}
45
56
  </span>
@@ -105,6 +116,8 @@
105
116
  text-overflow: ellipsis;
106
117
  white-space: nowrap;
107
118
  }
119
+ /* Room for the rename field's border, which the truncating title would clip. */
120
+ .terminoir-list-item__title--editable { padding: 1px 0.3rem; margin: -1px -0.3rem; }
108
121
  .terminoir-list-item__description {
109
122
  font-size: 0.75rem;
110
123
  color: var(--muted-foreground);
@@ -15,6 +15,10 @@ type $$ComponentProps = {
15
15
  details?: Snippet;
16
16
  /** Replaces title/description for custom content. */
17
17
  children?: Snippet;
18
+ /** Makes the title editable in place (rows without `onclick` only). */
19
+ onrename?: (title: string) => void | Promise<void>;
20
+ /** Accessible name of the title field while renaming. */
21
+ renameLabel?: string;
18
22
  } & Record<string, unknown>;
19
23
  declare const ListItem: import("svelte").Component<$$ComponentProps, {}, "">;
20
24
  type ListItem = ReturnType<typeof ListItem>;
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
3
  import { cn } from '../utils/cn.js';
4
+ import EditableText from './EditableText.svelte';
4
5
 
5
6
  let {
6
7
  title,
@@ -9,6 +10,8 @@
9
10
  leading,
10
11
  meta,
11
12
  actions,
13
+ onrename,
14
+ renameLabel = 'Title',
12
15
  }: {
13
16
  title: string;
14
17
  description?: string;
@@ -19,6 +22,10 @@
19
22
  meta?: Snippet;
20
23
  /** Buttons aligned to the right. */
21
24
  actions?: Snippet;
25
+ /** Makes the title editable in place: click it, type, Enter or click away to save, Escape to cancel. */
26
+ onrename?: (title: string) => void | Promise<void>;
27
+ /** Accessible name of the title field while renaming. */
28
+ renameLabel?: string;
22
29
  } = $props();
23
30
  </script>
24
31
 
@@ -27,7 +34,9 @@
27
34
  <div class="terminoir-page-header__leading">{@render leading()}</div>
28
35
  {/if}
29
36
  <div class="terminoir-page-header__text">
30
- <h1 class="terminoir-page-header__title">{title}</h1>
37
+ <h1 class="terminoir-page-header__title">
38
+ {#if onrename}<EditableText value={title} label={renameLabel} {onrename} />{:else}{title}{/if}
39
+ </h1>
31
40
  {#if meta}<div class="terminoir-page-header__meta">{@render meta()}</div>{/if}
32
41
  {#if description}<p class="terminoir-page-header__description">{description}</p>{/if}
33
42
  </div>
@@ -9,6 +9,10 @@ type $$ComponentProps = {
9
9
  meta?: Snippet;
10
10
  /** Buttons aligned to the right. */
11
11
  actions?: Snippet;
12
+ /** Makes the title editable in place: click it, type, Enter or click away to save, Escape to cancel. */
13
+ onrename?: (title: string) => void | Promise<void>;
14
+ /** Accessible name of the title field while renaming. */
15
+ renameLabel?: string;
12
16
  };
13
17
  declare const PageHeader: import("svelte").Component<$$ComponentProps, {}, "">;
14
18
  type PageHeader = ReturnType<typeof PageHeader>;
package/dist/index.d.ts CHANGED
@@ -84,6 +84,7 @@ export { default as SearchPicker } from './components/SearchPicker.svelte';
84
84
  export { default as MarkdownEditor } from './components/MarkdownEditor.svelte';
85
85
  export { default as FileButton } from './components/FileButton.svelte';
86
86
  export { default as TitleInput } from './components/TitleInput.svelte';
87
+ export { default as EditableText } from './components/EditableText.svelte';
87
88
  export type { PickerOption } from './utils/componentTypes.js';
88
89
  export { default as RadioCards } from './components/RadioCards.svelte';
89
90
  export type { RadioCardOption } from './utils/componentTypes.js';
package/dist/index.js CHANGED
@@ -76,6 +76,7 @@ export { default as SearchPicker } from './components/SearchPicker.svelte';
76
76
  export { default as MarkdownEditor } from './components/MarkdownEditor.svelte';
77
77
  export { default as FileButton } from './components/FileButton.svelte';
78
78
  export { default as TitleInput } from './components/TitleInput.svelte';
79
+ export { default as EditableText } from './components/EditableText.svelte';
79
80
  export { default as RadioCards } from './components/RadioCards.svelte';
80
81
  export { default as SegmentedControl } from './components/SegmentedControl.svelte';
81
82
  // Overlays
@@ -0,0 +1,2 @@
1
+ /** The name to save from an inline rename draft, or null when nothing should change (blank or unchanged). */
2
+ export declare function renamedValue(draft: string, current: string): string | null;
@@ -0,0 +1,5 @@
1
+ /** The name to save from an inline rename draft, or null when nothing should change (blank or unchanged). */
2
+ export function renamedValue(draft, current) {
3
+ const next = draft.trim();
4
+ return next && next !== current ? next : null;
5
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminoir",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Terminoir — Liquid Glass design tokens and Svelte 5 UI components.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",