@streamscloud/kit 0.10.7-1781507721211 → 0.10.7-1781511135617

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,14 +1,17 @@
1
- <script lang="ts">const { variant = 'neutral', size = 'md', style = 'soft', fullWidth = false, children } = $props();
2
- export {};
1
+ <script lang="ts">import { IconSlot } from '../icon';
2
+ const { variant = 'neutral', size = 'md', style = 'soft', fullWidth = false, icon, children } = $props();
3
3
  </script>
4
4
 
5
5
  <span class="badge badge--{size} badge--{variant}-{style}" class:badge--full={fullWidth}>
6
+ {#if icon}
7
+ <span class="badge__icon" aria-hidden="true"><IconSlot icon={icon} /></span>
8
+ {/if}
6
9
  <span class="badge__content">{@render children()}</span>
7
10
  </span>
8
11
 
9
12
  <!--
10
13
  @component
11
- Pill-shaped status indicator. Use `solid` for stronger emphasis, `soft` (default) for a calm tinted fill that pairs with body content.
14
+ Pill-shaped status indicator. Use `solid` for stronger emphasis, `soft` (default) for a calm tinted fill that pairs with body content. Optional leading `icon` renders in its own slot; the text content keeps its own ellipsis when it overflows.
12
15
 
13
16
  ### CSS Custom Properties
14
17
  | Property | Description | Default |
@@ -38,9 +41,14 @@ Pill-shaped status indicator. Use `solid` for stronger emphasis, `soft` (default
38
41
  background: var(--_badge--background);
39
42
  font-size: var(--_badge--font-size);
40
43
  font-weight: var(--_badge--font-weight);
41
- line-height: 1.4;
44
+ line-height: var(--sc-kit--leading--tight);
42
45
  max-width: 100%;
43
46
  }
47
+ .badge__icon {
48
+ display: inline-flex;
49
+ align-items: center;
50
+ flex-shrink: 0;
51
+ }
44
52
  .badge__content {
45
53
  min-width: 0;
46
54
  overflow: hidden;
@@ -1,3 +1,4 @@
1
+ import { type IconProp } from '../icon';
1
2
  import type { BadgeVariant } from './types';
2
3
  import type { Snippet } from 'svelte';
3
4
  type Props = {
@@ -9,10 +10,12 @@ type Props = {
9
10
  style?: 'soft' | 'solid';
10
11
  /** Stretch to fill the parent's inline axis, centering the content. @default false */
11
12
  fullWidth?: boolean;
13
+ /** Optional leading icon. */
14
+ icon?: IconProp;
12
15
  children: Snippet;
13
16
  };
14
17
  /**
15
- * Pill-shaped status indicator. Use `solid` for stronger emphasis, `soft` (default) for a calm tinted fill that pairs with body content.
18
+ * Pill-shaped status indicator. Use `solid` for stronger emphasis, `soft` (default) for a calm tinted fill that pairs with body content. Optional leading `icon` renders in its own slot; the text content keeps its own ellipsis when it overflows.
16
19
  *
17
20
  * ### CSS Custom Properties
18
21
  * | Property | Description | Default |
@@ -1,16 +1,14 @@
1
1
  <script lang="ts">import { Badge } from '../../badge';
2
- import { IconSlot } from '../../icon';
3
2
  import GridCardField from './cmp.grid-card-field.svelte';
4
3
  const { label, text, variant = 'neutral', style = 'soft', icon } = $props();
5
4
  </script>
6
5
 
7
6
  <GridCardField label={label}>
8
- <Badge variant={variant} style={style} size="sm">
9
- {#if icon}
10
- <span class="grid-card-badge-field__icon" aria-hidden="true"><IconSlot icon={icon} /></span>
11
- {/if}
12
- {text}
13
- </Badge>
7
+ <span class="grid-card-badge-field">
8
+ <Badge variant={variant} style={style} size="sm" icon={icon}>
9
+ {text}
10
+ </Badge>
11
+ </span>
14
12
  </GridCardField>
15
13
 
16
14
  <!--
@@ -20,9 +18,7 @@ inside a `<GridCardField>` so it aligns with text rows. Use for content status,
20
18
  plan / tier indicators, etc.
21
19
  -->
22
20
 
23
- <style>.grid-card-badge-field__icon {
24
- display: inline-flex;
25
- align-items: center;
21
+ <style>.grid-card-badge-field {
22
+ display: contents;
26
23
  --sc-kit--icon--size: 0.875em;
27
- --sc-kit--icon--color: currentColor;
28
24
  }</style>
@@ -1,5 +1,5 @@
1
1
  import type { BadgeVariant } from '../../badge';
2
- import { type IconProp } from '../../icon';
2
+ import type { IconProp } from '../../icon';
3
3
  type Props = {
4
4
  label: string;
5
5
  text: string;
@@ -25,9 +25,14 @@ export declare const buildFuseIndex: <T>(items: SelectItem<T>[]) => Fuse<FlatOpt
25
25
  * shape so the core's group-header logic keeps working. Empty query returns the original
26
26
  * items untouched.
27
27
  *
28
+ * Results are ordered by relevance, not by the original list order: matches rank
29
+ * prefix > substring > scattered-fuzzy (the Fuse score breaks ties within a tier), groups
30
+ * are ordered by their best hit, and options keep that order inside each group. Scattered
31
+ * fuzzy hits are a typo fallback only — they are dropped whenever any prefix/substring hit
32
+ * exists, so a real match is never buried under noise.
33
+ *
28
34
  * Group labels participate in the match: when a group's label fuzzy-matches the query, the
29
35
  * entire group (header + every child) is included regardless of whether the children
30
- * themselves matched. Matched children also bubble their parent group into the result so
31
- * the tree structure stays readable.
36
+ * themselves matched.
32
37
  */
33
38
  export declare const runFuseSearch: <T>(fuse: Fuse<FlatOption<T>>, query: string, items: SelectItem<T>[]) => SelectItem<T>[];
@@ -52,56 +52,72 @@ export const moveHighlightIndex = (indices, current, delta) => {
52
52
  * locally inside their `loadOptions` shim. Async impls don't need this — the server does
53
53
  * the filtering.
54
54
  */
55
- export const buildFuseIndex = (items) => new Fuse(flattenItems(items), { keys: ['option.label'], threshold: 0.4, ignoreLocation: true });
55
+ export const buildFuseIndex = (items) => new Fuse(flattenItems(items), { keys: ['option.label'], threshold: 0.4, ignoreLocation: true, includeScore: true });
56
56
  /**
57
57
  * Runs a fuzzy search via the Fuse index and reconstructs the grouped `SelectItem<T>[]`
58
58
  * shape so the core's group-header logic keeps working. Empty query returns the original
59
59
  * items untouched.
60
60
  *
61
+ * Results are ordered by relevance, not by the original list order: matches rank
62
+ * prefix > substring > scattered-fuzzy (the Fuse score breaks ties within a tier), groups
63
+ * are ordered by their best hit, and options keep that order inside each group. Scattered
64
+ * fuzzy hits are a typo fallback only — they are dropped whenever any prefix/substring hit
65
+ * exists, so a real match is never buried under noise.
66
+ *
61
67
  * Group labels participate in the match: when a group's label fuzzy-matches the query, the
62
68
  * entire group (header + every child) is included regardless of whether the children
63
- * themselves matched. Matched children also bubble their parent group into the result so
64
- * the tree structure stays readable.
69
+ * themselves matched.
65
70
  */
66
71
  export const runFuseSearch = (fuse, query, items) => {
67
72
  if (!query) {
68
73
  return items;
69
74
  }
70
- const hits = fuse.search(query).map((r) => r.item);
75
+ const needle = query.toLowerCase();
76
+ const rankOf = (label, score) => {
77
+ const haystack = label.toLowerCase();
78
+ const tier = haystack.startsWith(needle) ? 0 : haystack.includes(needle) ? 1 : 2;
79
+ return tier + score;
80
+ };
81
+ const ranked = fuse
82
+ .search(query)
83
+ .map((r) => ({ flat: r.item, rank: rankOf(r.item.option.label, r.score ?? 1) }))
84
+ .sort((a, b) => a.rank - b.rank);
85
+ const hasExact = ranked.some((r) => r.rank < 2);
86
+ const matches = hasExact ? ranked.filter((r) => r.rank < 2) : ranked;
87
+ const groups = items.filter(isSelectGroup);
88
+ const labelMatches = new Set();
89
+ if (groups.length > 0) {
90
+ const labelFuse = new Fuse(groups.map((g) => ({ label: g.label })), { keys: ['label'], threshold: 0.4, ignoreLocation: true });
91
+ for (const hit of labelFuse.search(query)) {
92
+ labelMatches.add(hit.item.label);
93
+ }
94
+ }
71
95
  const groupBuckets = new Map();
96
+ const groupRank = new Map();
72
97
  const standalone = [];
73
- for (const hit of hits) {
74
- if (hit.group) {
75
- const bucket = groupBuckets.get(hit.group);
98
+ matches.forEach(({ flat }, i) => {
99
+ if (flat.group) {
100
+ const bucket = groupBuckets.get(flat.group);
76
101
  if (bucket) {
77
- bucket.push(hit.option);
102
+ bucket.push(flat.option);
78
103
  }
79
104
  else {
80
- groupBuckets.set(hit.group, [hit.option]);
105
+ groupBuckets.set(flat.group, [flat.option]);
106
+ groupRank.set(flat.group, i);
81
107
  }
82
108
  }
83
109
  else {
84
- standalone.push(hit.option);
110
+ standalone.push(flat.option);
85
111
  }
86
- }
87
- // Fuzzy-match group labels: build a tiny per-call Fuse just over group label entries.
88
- // Cheap (one item per group) and reuses the same threshold so behavior matches options.
89
- const groups = items.filter(isSelectGroup);
90
- const labelMatches = new Set();
91
- if (groups.length > 0) {
92
- const labelFuse = new Fuse(groups.map((g) => ({ label: g.label })), { keys: ['label'], threshold: 0.4, ignoreLocation: true });
93
- for (const hit of labelFuse.search(query)) {
94
- labelMatches.add(hit.item.label);
95
- }
96
- }
112
+ });
97
113
  const out = [];
98
- for (const item of items) {
99
- if (isSelectGroup(item)) {
100
- // Group label matched include every child. Otherwise include only the children that matched.
101
- const childrenForRender = labelMatches.has(item.label) ? item.options : groupBuckets.get(item.label);
102
- if (childrenForRender && childrenForRender.length > 0) {
103
- out.push({ label: item.label, value: item.value, options: childrenForRender });
104
- }
114
+ const matchedGroups = groups
115
+ .filter((g) => groupBuckets.has(g.label) || labelMatches.has(g.label))
116
+ .sort((a, b) => (groupRank.get(a.label) ?? Infinity) - (groupRank.get(b.label) ?? Infinity));
117
+ for (const g of matchedGroups) {
118
+ const options = labelMatches.has(g.label) ? g.options : (groupBuckets.get(g.label) ?? []);
119
+ if (options.length > 0) {
120
+ out.push({ label: g.label, value: g.value, options });
105
121
  }
106
122
  }
107
123
  for (const opt of standalone) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamscloud/kit",
3
- "version": "0.10.7-1781507721211",
3
+ "version": "0.10.7-1781511135617",
4
4
  "author": "StreamsCloud",
5
5
  "repository": {
6
6
  "type": "git",