@heyramzi/ui 2.10.0 → 2.11.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.
@@ -1,2 +1,2 @@
1
1
  export { default as TagsInput } from "../../../src/components/tags-input/tags-input.svelte";
2
- export { addTag, canCreate, cleanTags, collectOptions, editDistance, findOption, fold, matchOptions, removeTag, splitTags } from "./tags.js";
2
+ export { addTag, canCreate, cleanTags, collectOptions, editDistance, findOption, fold, matchOptions, removeTag, splitTags, splitTyped } from "./tags.js";
@@ -1,6 +1,9 @@
1
1
  export function splitTags(text) {
2
2
  return text.split(/[,\n]/).map((part)=>part.trim()).filter(Boolean);
3
3
  }
4
+ export function splitTyped(text) {
5
+ return text.split(/[\s,;]+/).filter(Boolean);
6
+ }
4
7
  export function fold(text) {
5
8
  return text.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase().replace(/[\s._-]+/g, "");
6
9
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyramzi/ui",
3
- "version": "2.10.0",
3
+ "version": "2.11.0",
4
4
  "description": "Svelte 5 UI for apps: primitives, effects and animations, plus work (tasks and docs), charts, slides, an editor, onboarding and a canvas as subpaths.",
5
5
  "homepage": "https://ui.heyramzi.com",
6
6
  "license": "MIT",
@@ -383,7 +383,7 @@
383
383
  }
384
384
  },
385
385
  "dependencies": {
386
- "@dagrejs/dagre": "^3.1.1",
386
+ "@dagrejs/dagre": "^3.1.3",
387
387
  "@shadcn-svelte/primitives": "^0.0.1",
388
388
  "@tiptap/core": "^3.32.0",
389
389
  "@tiptap/extension-drag-handle": "^3.32.0",
@@ -398,9 +398,9 @@
398
398
  "clsx": "^2.1.1",
399
399
  "d3-scale": "^4.0.2",
400
400
  "d3-shape": "^3.2.0",
401
- "layerchart": "^2.8.1",
401
+ "layerchart": "^2.9.1",
402
402
  "markdown-it": "^15.0.3",
403
- "marked": "^18.1.1",
403
+ "marked": "^18.1.2",
404
404
  "perfect-freehand": "^1.2.3",
405
405
  "phosphor-svelte": "^3.1.1",
406
406
  "roughjs": "^4.6.6",
@@ -416,14 +416,14 @@
416
416
  "@types/d3-shape": "^3.2.0",
417
417
  "@types/node": "^26.6.5",
418
418
  "@xyflow/svelte": "^1.7.0",
419
- "bits-ui": "^2.20.0",
419
+ "bits-ui": "^2.20.2",
420
420
  "jsdom": "^30.1.3",
421
421
  "mode-watcher": "^1.1.0",
422
- "svelte": "^5.57.2",
422
+ "svelte": "^5.57.3",
423
423
  "svelte-check": "^4.7.7",
424
424
  "svelte-sonner": "^1.2.1",
425
425
  "typescript": "~7.0.2",
426
- "vite-plus": "1.3.0"
426
+ "vite-plus": "1.4.0"
427
427
  },
428
428
  "peerDependencies": {
429
429
  "@internationalized/date": "^3.12.4",
@@ -10,4 +10,5 @@ export {
10
10
  matchOptions,
11
11
  removeTag,
12
12
  splitTags,
13
+ splitTyped,
13
14
  } from "./tags.js";
@@ -11,6 +11,7 @@
11
11
  matchOptions,
12
12
  removeTag,
13
13
  splitTags,
14
+ splitTyped,
14
15
  } from "./tags.js";
15
16
 
16
17
  // =========================
@@ -40,6 +41,11 @@
40
41
  allowCreate?: boolean;
41
42
  /** Most tags the list holds. Past it the input goes away. */
42
43
  max?: number;
44
+ /**
45
+ * Tags typed freely, with no options and no menu: Enter, comma, space or Tab adds what's
46
+ * typed, a pasted list adds every part, and leaving the field keeps what was typed.
47
+ */
48
+ free?: boolean;
43
49
  /** Accessible name of the field. */
44
50
  label: string;
45
51
  labels?: Partial<Labels>;
@@ -56,6 +62,7 @@
56
62
  options = [],
57
63
  allowCreate = true,
58
64
  max,
65
+ free = false,
59
66
  label,
60
67
  labels,
61
68
  disabled = false,
@@ -90,11 +97,17 @@
90
97
  const matches = $derived(matchOptions(options, tags, query));
91
98
  const creatable = $derived(canCreate(options, tags, query, allowCreate));
92
99
  // Options closest first (a typo's fix comes before anything new), then the one row that makes what's typed.
93
- const rows = $derived([
94
- ...matches.map((name) => ({ kind: "pick" as const, name })),
95
- ...(creatable ? [{ kind: "create" as const, name: query.trim() }] : []),
96
- ]);
97
- const nothing = $derived(editing && !full && query.trim() !== "" && rows.length === 0);
100
+ const rows = $derived(
101
+ free
102
+ ? []
103
+ : [
104
+ ...matches.map((name) => ({ kind: "pick" as const, name })),
105
+ ...(creatable ? [{ kind: "create" as const, name: query.trim() }] : []),
106
+ ],
107
+ );
108
+ const nothing = $derived(
109
+ !free && editing && !full && query.trim() !== "" && rows.length === 0,
110
+ );
98
111
  const open = $derived(editing && !full && (rows.length > 0 || nothing));
99
112
 
100
113
  // =========================
@@ -117,12 +130,12 @@
117
130
  }
118
131
 
119
132
  // Leaving the field keeps what was typed only when it is an existing label said another way.
120
- // Anything else is dropped: free text is never saved by walking away.
133
+ // Anything else is dropped unless the field is free: free text is never saved by walking away.
121
134
  function stop() {
122
135
  const typed = query;
123
136
  query = "";
124
137
  editing = false;
125
- const match = findOption(options, typed);
138
+ const match = free ? typed.trim() : findOption(options, typed);
126
139
  if (match) void commit(addTag(tags, match, options, max));
127
140
  }
128
141
 
@@ -134,7 +147,12 @@
134
147
 
135
148
  function onKey(event: KeyboardEvent) {
136
149
  const last = rows.length - 1;
137
- if (event.key === "ArrowDown" || event.key === "ArrowUp") {
150
+ const separator = event.key === "," || (free && event.key === " ");
151
+ if (free && (separator || event.key === "Enter" || (event.key === "Tab" && query.trim()))) {
152
+ event.preventDefault();
153
+ if (query.trim()) add(query);
154
+ else if (event.key === "Enter") stop();
155
+ } else if (event.key === "ArrowDown" || event.key === "ArrowUp") {
138
156
  if (rows.length === 0) return;
139
157
  event.preventDefault();
140
158
  const step = event.key === "ArrowDown" ? 1 : -1;
@@ -162,6 +180,7 @@
162
180
  // Pasted words that are existing labels are added. The rest stay in the box to be created on purpose.
163
181
  function onPaste(event: ClipboardEvent) {
164
182
  const pasted = event.clipboardData?.getData("text") ?? "";
183
+ if (free) return pasteTyped(event, pasted);
165
184
  if (!/[,\n]/.test(pasted)) return;
166
185
  event.preventDefault();
167
186
  const parts = splitTags(pasted);
@@ -173,6 +192,17 @@
173
192
  );
174
193
  }
175
194
 
195
+ // A free field takes a pasted list whole, one tag per part. One word pastes into the box as usual.
196
+ function pasteTyped(event: ClipboardEvent, pasted: string) {
197
+ const parts = splitTyped(pasted);
198
+ if (parts.length < 2) return;
199
+ event.preventDefault();
200
+ query = "";
201
+ void commit(
202
+ parts.reduce((list, part) => addTag(list, part, options, max), tags as readonly string[]),
203
+ );
204
+ }
205
+
176
206
  function onFocusOut(event: FocusEvent) {
177
207
  const next = event.relatedTarget as Node | null;
178
208
  if (editing && !(next && root?.contains(next))) stop();
@@ -8,6 +8,7 @@ import {
8
8
  matchOptions,
9
9
  removeTag,
10
10
  splitTags,
11
+ splitTyped,
11
12
  } from "./tags.js";
12
13
 
13
14
  const options = ["Airtable", "Docusign", "QuickBooks", "SharePoint", "Supabase"];
@@ -17,6 +18,16 @@ describe("tags as labels", () => {
17
18
  expect(splitTags(" a, b,,\nc ,")).toEqual(["a", "b", "c"]);
18
19
  });
19
20
 
21
+ // save-to-clickup, 28 Oct 2026: client domains pasted from anywhere, separated any way.
22
+ it("splits a free field's paste on commas, semicolons, newlines and spaces", () => {
23
+ expect(splitTyped("acme.com, bo.io;\nzed.co ann@x.dev")).toEqual([
24
+ "acme.com",
25
+ "bo.io",
26
+ "zed.co",
27
+ "ann@x.dev",
28
+ ]);
29
+ });
30
+
20
31
  it("counts a transposition as one edit", () => {
21
32
  expect(editDistance("airtbale", "airtable")).toBe(1);
22
33
  expect(editDistance("make", "airtable")).toBeGreaterThan(2);
@@ -9,6 +9,11 @@ export function splitTags(text: string): string[] {
9
9
  .filter(Boolean);
10
10
  }
11
11
 
12
+ /** Splits typed or pasted text for a free field: commas, semicolons, newlines and spaces all separate. */
13
+ export function splitTyped(text: string): string[] {
14
+ return text.split(/[\s,;]+/).filter(Boolean);
15
+ }
16
+
12
17
  /** What two spellings are compared by: no case, no spaces, no marks ("Share Point" is "SharePoint"). */
13
18
  export function fold(text: string): string {
14
19
  return text
@@ -54,7 +54,6 @@
54
54
  let dasharray = $derived(
55
55
  fraction != null ? `${INNER_CIRC * fraction} ${INNER_CIRC * (1 - fraction)}` : undefined,
56
56
  );
57
- let dashoffset = $derived(fraction != null ? INNER_CIRC * 0.25 : undefined);
58
57
  </script>
59
58
 
60
59
  {#if resolvedVariant === "completed"}
@@ -128,7 +127,6 @@
128
127
  stroke={resolvedColor}
129
128
  stroke-width="4"
130
129
  stroke-dasharray={dasharray}
131
- stroke-dashoffset={dashoffset}
132
130
  transform="rotate(-90 7 7)"
133
131
  class={animate ? "sd-fill" : ""}
134
132
  ></circle>