@mhmo91/schmancy 0.9.26 → 0.9.27

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.
@@ -203,7 +203,7 @@ The manifest already has everything needed; the package is just the JSON-RPC wra
203
203
 
204
204
  **Problem.** `handover/agent-runtime-v1.md` had `<PENDING>` placeholders that we manually replaced with `0.9.13` after the first publish. Future handover docs will have the same issue.
205
205
 
206
- **Fix.** A build step that substitutes `0.9.26` in `handover/**/*.md` against `package.json`'s `version` field on every build. `dist/handover/**/*.md` gets the rendered version; the source stays templated.
206
+ **Fix.** A build step that substitutes `0.9.27` in `handover/**/*.md` against `package.json`'s `version` field on every build. `dist/handover/**/*.md` gets the rendered version; the source stays templated.
207
207
 
208
208
  **Effort:** ~30 min (one sed step or a tiny script).
209
209
 
@@ -7,8 +7,8 @@
7
7
  ## The URLs you asked for
8
8
 
9
9
  ```
10
- https://esm.sh/@mhmo91/schmancy/agent@0.9.26
11
- https://esm.sh/@mhmo91/schmancy/agent/manifest@0.9.26
10
+ https://esm.sh/@mhmo91/schmancy/agent@0.9.27
11
+ https://esm.sh/@mhmo91/schmancy/agent/manifest@0.9.27
12
12
  ```
13
13
 
14
14
  `0.9.13` is the first release containing `/agent`; every subsequent publish serves the same subpath. `npm view @mhmo91/schmancy version` always returns the current pin if you want to float forward.
@@ -20,7 +20,7 @@ One script tag. No bundler, no bare specifiers, no npm install.
20
20
  ```html
21
21
  <!doctype html>
22
22
  <script type="module">
23
- import { $dialog, theme } from 'https://esm.sh/@mhmo91/schmancy/agent@0.9.26';
23
+ import { $dialog, theme } from 'https://esm.sh/@mhmo91/schmancy/agent@0.9.27';
24
24
  </script>
25
25
  <schmancy-theme root scheme="dark">
26
26
  <schmancy-surface type="solid" fill="all">
@@ -12,7 +12,7 @@ Four separable PRs, each shippable on its own:
12
12
  | # | Title | Branch | Impact on your probe |
13
13
  |---|---|---|---|
14
14
  | 2 | CI smoke-test gate | `feat/ci-gate-and-version-templating` | None user-facing. `window.schmancy.help()` regressions now fail-closed at publish time instead of shipping silently. |
15
- | 9 | `0.9.26` templating for handover docs | same branch as #2 | None user-facing. Future handover docs will have live esm.sh URLs instead of `<PENDING>` placeholders. |
15
+ | 9 | `0.9.27` templating for handover docs | same branch as #2 | None user-facing. Future handover docs will have live esm.sh URLs instead of `<PENDING>` placeholders. |
16
16
  | 3 | JSDoc backfill (46 components) | `feat/jsdoc-batch-{1,2,3}-*` | **This is what you'll notice.** Every form-control, container, and overlay/nav component now ships `@summary`, `@example`, and `@platform` tags in its manifest entry. `window.schmancy.help('schmancy-button')` returns a non-empty `summary`, a copy-pastable `examples[]`, and a `platformPrimitive` hint for graceful degradation. |
17
17
  | 1 | Lazy vendor chunks | `feat/lazy-{typewriter,code-highlight,qr-scanner}` | Pages that don't render `<schmancy-code>`, `<schmancy-qr-scanner>`, or `<schmancy-typewriter>` no longer fetch `vendor-highlight`, `vendor-jsqr`, or the typewriter chunk on first paint. ~68 KB gzipped saved on cold starts for typical prototypes. |
18
18
 
@@ -21,18 +21,18 @@ The only one that changes the shape of `window.schmancy` is **#3**. The others a
21
21
  ## Pinned URLs (live once the PRs merge)
22
22
 
23
23
  ```
24
- https://esm.sh/@mhmo91/schmancy/agent@0.9.26
25
- https://esm.sh/@mhmo91/schmancy/agent/manifest@0.9.26
24
+ https://esm.sh/@mhmo91/schmancy/agent@0.9.27
25
+ https://esm.sh/@mhmo91/schmancy/agent/manifest@0.9.27
26
26
  ```
27
27
 
28
- `0.9.26` is substituted at publish time — see [`agent-runtime-followups.md`](./agent-runtime-followups.md) #9. Until the PRs land, continue pinning `@0.9.14` (the last published version at time of writing).
28
+ `0.9.27` is substituted at publish time — see [`agent-runtime-followups.md`](./agent-runtime-followups.md) #9. Until the PRs land, continue pinning `@0.9.14` (the last published version at time of writing).
29
29
 
30
30
  ## Minimum loop-back test
31
31
 
32
32
  ```html
33
33
  <!doctype html>
34
34
  <script type="module">
35
- import 'https://esm.sh/@mhmo91/schmancy/agent@0.9.26';
35
+ import 'https://esm.sh/@mhmo91/schmancy/agent@0.9.27';
36
36
  </script>
37
37
 
38
38
  <schmancy-theme root scheme="dark">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhmo91/schmancy",
3
- "version": "0.9.26",
3
+ "version": "0.9.27",
4
4
  "description": "UI library build with web components",
5
5
  "main": "./dist/index.js",
6
6
  "customElements": "custom-elements.json",
@@ -89,4 +89,53 @@ describe('agent bundle — handover §4.4 acceptance', () => {
89
89
 
90
90
  skill.remove()
91
91
  })
92
+
93
+ /**
94
+ * findFor() is the discovery-failure backstop. Phase 1 of the manifest-as-
95
+ * validator-data plan: agents that reach for a "missing" component should
96
+ * call this first and find what already ships.
97
+ */
98
+ it('`window.schmancy.findFor("badge")` returns schmancy-badge first', async () => {
99
+ const skill = document.createElement('schmancy-skill')
100
+ document.body.appendChild(skill)
101
+ await customElements.whenDefined('schmancy-skill')
102
+ await new Promise(requestAnimationFrame)
103
+
104
+ const results = window.schmancy!.findFor('badge') as Array<{
105
+ tag: string
106
+ score: number
107
+ summary?: string
108
+ }>
109
+ expect(results.length).toBeGreaterThan(0)
110
+ expect(results[0].tag).toBe('schmancy-badge')
111
+ expect(results[0].score).toBeGreaterThan(0)
112
+
113
+ skill.remove()
114
+ })
115
+
116
+ it('`window.schmancy.findFor("avatar")` returns schmancy-avatar first', async () => {
117
+ const skill = document.createElement('schmancy-skill')
118
+ document.body.appendChild(skill)
119
+ await customElements.whenDefined('schmancy-skill')
120
+ await new Promise(requestAnimationFrame)
121
+
122
+ const results = window.schmancy!.findFor('avatar') as Array<{ tag: string; score: number }>
123
+ expect(results.length).toBeGreaterThan(0)
124
+ expect(results[0].tag).toBe('schmancy-avatar')
125
+
126
+ skill.remove()
127
+ })
128
+
129
+ it('`window.schmancy.findFor("xyz123nonexistent")` returns empty array', async () => {
130
+ const skill = document.createElement('schmancy-skill')
131
+ document.body.appendChild(skill)
132
+ await customElements.whenDefined('schmancy-skill')
133
+ await new Promise(requestAnimationFrame)
134
+
135
+ const results = window.schmancy!.findFor('xyz123nonexistent') as unknown[]
136
+ expect(Array.isArray(results)).toBe(true)
137
+ expect(results.length).toBe(0)
138
+
139
+ skill.remove()
140
+ })
92
141
  })
@@ -19,6 +19,7 @@ export type ElementEntry = {
19
19
  slots?: Array<{ name: string; description?: string }>
20
20
  cssProperties?: Array<{ name: string; description?: string }>
21
21
  cssParts?: Array<{ name: string; description?: string }>
22
+ examples?: string[]
22
23
  whenToUse?: string
23
24
  platformPrimitive?: { tag: string; mode?: string; note?: string }
24
25
  contexts?: { provides?: string[]; consumes?: string[] }
@@ -72,6 +73,99 @@ export function tokens(): string[] {
72
73
  return (manifest as { tokens?: string[] }).tokens ?? []
73
74
  }
74
75
 
76
+ // --- findFor: keyword search over the manifest -------------------------------
77
+
78
+ /**
79
+ * Stopwords stripped from both query and document tokens. Kept short on
80
+ * purpose — over-aggressive stopword lists hurt recall on short queries
81
+ * like "use". Word "use" stays in because of "use case" matches.
82
+ */
83
+ const STOPWORDS = new Set([
84
+ 'a', 'an', 'and', 'or', 'the', 'of', 'in', 'on', 'at', 'to', 'for', 'with',
85
+ 'is', 'it', 'this', 'that', 'these', 'those', 'be', 'by', 'as', 'are', 'was',
86
+ 'i', 'you', 'we', 'my', 'your',
87
+ ])
88
+
89
+ function tokenize(text: string): Set<string> {
90
+ const out = new Set<string>()
91
+ for (const t of text.toLowerCase().match(/[a-z][a-z0-9]+/g) ?? []) {
92
+ if (t.length >= 2 && !STOPWORDS.has(t)) out.add(t)
93
+ }
94
+ return out
95
+ }
96
+
97
+ type IndexEntry = {
98
+ entry: ElementEntry
99
+ body: Set<string> // tokens from summary + description + examples
100
+ tagTokens: Set<string> // tokens derived from the tag name itself
101
+ }
102
+
103
+ let _searchIndex: IndexEntry[] | null = null
104
+
105
+ function buildSearchIndex(): IndexEntry[] {
106
+ return elements().map(entry => {
107
+ const tagTokens = tokenize((entry.tagName ?? '').replace(/-/g, ' '))
108
+ const body = tokenize(
109
+ [
110
+ entry.tagName ?? '',
111
+ entry.summary ?? '',
112
+ entry.description ?? '',
113
+ entry.whenToUse ?? '',
114
+ (entry.examples ?? []).join(' '),
115
+ ].join(' '),
116
+ )
117
+ return { entry, body, tagTokens }
118
+ })
119
+ }
120
+
121
+ export type FindForResult = {
122
+ tag: string
123
+ score: number
124
+ summary?: string
125
+ examples?: string[]
126
+ }
127
+
128
+ /**
129
+ * Keyword search over the component manifest. Tokenizes the query the same
130
+ * way each component's `summary + description + examples` were tokenized,
131
+ * and returns the components with the most overlap. Tag-name token matches
132
+ * count for 3× a body-text match.
133
+ *
134
+ * Designed to catch the "I'm reaching for a custom component, what does
135
+ * schmancy ship?" gap without bringing in a vector-embedding dependency.
136
+ *
137
+ * @example
138
+ * window.schmancy.findFor('status pill')
139
+ * // → [{ tag: 'schmancy-badge', score: 4, summary: '…', examples: [...] }]
140
+ *
141
+ * window.schmancy.findFor('initials avatar')
142
+ * // → [{ tag: 'schmancy-avatar', score: 5, ... }]
143
+ *
144
+ * window.schmancy.findFor('xyz123nonexistent') // → []
145
+ */
146
+ export function findFor(query: string, limit = 5): FindForResult[] {
147
+ if (!_searchIndex) _searchIndex = buildSearchIndex()
148
+ const queryTokens = tokenize(query)
149
+ if (queryTokens.size === 0) return []
150
+
151
+ const scored: Array<{ entry: ElementEntry; score: number }> = []
152
+ for (const ix of _searchIndex) {
153
+ let score = 0
154
+ for (const qt of queryTokens) {
155
+ if (ix.tagTokens.has(qt)) score += 3
156
+ else if (ix.body.has(qt)) score += 1
157
+ }
158
+ if (score > 0) scored.push({ entry: ix.entry, score })
159
+ }
160
+ scored.sort((a, b) => b.score - a.score)
161
+ return scored.slice(0, limit).map(({ entry, score }) => ({
162
+ tag: entry.tagName ?? '',
163
+ score,
164
+ summary: entry.summary,
165
+ examples: entry.examples,
166
+ }))
167
+ }
168
+
75
169
  export function platformPrimitive(tag: string): ElementEntry['platformPrimitive'] | null {
76
170
  return elements().find(e => e.tagName === tag)?.platformPrimitive ?? null
77
171
  }
@@ -4,6 +4,7 @@ import { customElement } from 'lit/decorators.js'
4
4
  import {
5
5
  a11yAudit,
6
6
  capabilities,
7
+ findFor,
7
8
  help,
8
9
  manifest,
9
10
  manifestUrl,
@@ -23,6 +24,7 @@ declare global {
23
24
  registeredTags: typeof registeredTags
24
25
  a11yAudit: typeof a11yAudit
25
26
  capabilities: typeof capabilities
27
+ findFor: typeof findFor
26
28
  }
27
29
  }
28
30
  }
@@ -42,6 +44,7 @@ function install() {
42
44
  registeredTags,
43
45
  a11yAudit,
44
46
  capabilities,
47
+ findFor,
45
48
  }
46
49
  }
47
50
 
@@ -34,6 +34,7 @@ export type ElementEntry = {
34
34
  name: string;
35
35
  description?: string;
36
36
  }>;
37
+ examples?: string[];
37
38
  whenToUse?: string;
38
39
  platformPrimitive?: {
39
40
  tag: string;
@@ -58,6 +59,31 @@ export type ServiceEntry = {
58
59
  };
59
60
  export declare function help(tag?: string): unknown;
60
61
  export declare function tokens(): string[];
62
+ export type FindForResult = {
63
+ tag: string;
64
+ score: number;
65
+ summary?: string;
66
+ examples?: string[];
67
+ };
68
+ /**
69
+ * Keyword search over the component manifest. Tokenizes the query the same
70
+ * way each component's `summary + description + examples` were tokenized,
71
+ * and returns the components with the most overlap. Tag-name token matches
72
+ * count for 3× a body-text match.
73
+ *
74
+ * Designed to catch the "I'm reaching for a custom component, what does
75
+ * schmancy ship?" gap without bringing in a vector-embedding dependency.
76
+ *
77
+ * @example
78
+ * window.schmancy.findFor('status pill')
79
+ * // → [{ tag: 'schmancy-badge', score: 4, summary: '…', examples: [...] }]
80
+ *
81
+ * window.schmancy.findFor('initials avatar')
82
+ * // → [{ tag: 'schmancy-avatar', score: 5, ... }]
83
+ *
84
+ * window.schmancy.findFor('xyz123nonexistent') // → []
85
+ */
86
+ export declare function findFor(query: string, limit?: number): FindForResult[];
61
87
  export declare function platformPrimitive(tag: string): ElementEntry['platformPrimitive'] | null;
62
88
  export declare function registeredTags(): string[];
63
89
  export declare function a11yAudit(): Array<{
@@ -1,4 +1,4 @@
1
- import { a11yAudit, capabilities, help, manifest, platformPrimitive, registeredTags, tokens } from './helpers';
1
+ import { a11yAudit, capabilities, findFor, help, manifest, platformPrimitive, registeredTags, tokens } from './helpers';
2
2
  declare global {
3
3
  interface Window {
4
4
  schmancy?: {
@@ -10,6 +10,7 @@ declare global {
10
10
  registeredTags: typeof registeredTags;
11
11
  a11yAudit: typeof a11yAudit;
12
12
  capabilities: typeof capabilities;
13
+ findFor: typeof findFor;
13
14
  };
14
15
  }
15
16
  }