@immediately-run/omnibox 0.1.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/dist/launch.js ADDED
@@ -0,0 +1,88 @@
1
+ const PROVIDERS = {
2
+ github: "github.com"
3
+ };
4
+ const DEFAULT_PROVIDER = "github";
5
+ function presentPathOf(provider, namespace, repository, ref) {
6
+ const base = `/present/${provider}/${namespace}/${repository}`;
7
+ return ref ? `${base}/${encodeURIComponent(ref)}` : base;
8
+ }
9
+ function location(provider, namespace, repository, ref) {
10
+ const display = ref ? `${provider}:${namespace}/${repository}@${ref}` : `${provider}:${namespace}/${repository}`;
11
+ return { kind: "location", provider, namespace, repository, ...ref ? { ref } : {}, display, presentPath: presentPathOf(provider, namespace, repository, ref) };
12
+ }
13
+ function providerLabel(hostname) {
14
+ return hostname.replace(/^www\./, "").split(".")[0];
15
+ }
16
+ function parseTuple(rest, provider) {
17
+ const slash = rest.indexOf("/");
18
+ if (slash <= 0) return null;
19
+ const namespace = rest.slice(0, slash);
20
+ const tail = rest.slice(slash + 1);
21
+ if (!namespace || !tail) return null;
22
+ const at = tail.indexOf("@");
23
+ const repository = at === -1 ? tail : tail.slice(0, at);
24
+ const ref = at === -1 ? void 0 : tail.slice(at + 1);
25
+ if (!repository || repository.includes("/")) return null;
26
+ if (ref !== void 0 && !ref) return null;
27
+ return location(provider, namespace, repository, ref);
28
+ }
29
+ function parseLaunch(input, defaultProvider = DEFAULT_PROVIDER) {
30
+ const raw = input.trim();
31
+ if (!raw) return { kind: "text", query: raw };
32
+ if (/^https?:\/\//i.test(raw)) {
33
+ let url;
34
+ try {
35
+ url = new URL(raw);
36
+ } catch {
37
+ return { kind: "text", query: raw };
38
+ }
39
+ const host = url.hostname.toLowerCase();
40
+ if (host === "immediately.run" || host.endsWith(".immediately.run")) {
41
+ if (/^\/(present|edit)(\/|$)/.test(url.pathname)) {
42
+ return { kind: "platform-url", path: url.pathname + url.search + url.hash };
43
+ }
44
+ return { kind: "text", query: raw };
45
+ }
46
+ const providerEntry = Object.entries(PROVIDERS).find(([, urlHost]) => urlHost === host);
47
+ if (providerEntry) {
48
+ const [provider] = providerEntry;
49
+ const segs2 = url.pathname.split("/").filter(Boolean);
50
+ if (segs2.length < 2) return { kind: "text", query: raw };
51
+ const [namespace, repo0] = segs2;
52
+ const repo = repo0.endsWith(".git") ? repo0.slice(0, -4) : repo0;
53
+ if (!repo) return { kind: "text", query: raw };
54
+ if (segs2.length > 2) {
55
+ const [marker, ...extra] = segs2.slice(2);
56
+ if (marker === "tree" && extra.length > 0) {
57
+ return location(provider, namespace, repo, extra.join("/"));
58
+ }
59
+ if (marker === "blob" && extra.length > 0) {
60
+ return location(provider, namespace, repo, extra[0]);
61
+ }
62
+ return { kind: "text", query: raw };
63
+ }
64
+ return location(provider, namespace, repo);
65
+ }
66
+ const segs = url.pathname.split("/").filter(Boolean);
67
+ if (segs.length >= 2) return { kind: "unknown-provider", provider: providerLabel(host) };
68
+ return { kind: "text", query: raw };
69
+ }
70
+ const colon = raw.indexOf(":");
71
+ if (colon > 0 && !raw.slice(0, colon).includes("/")) {
72
+ const provider = raw.slice(0, colon).toLowerCase();
73
+ const rest = raw.slice(colon + 1);
74
+ if (provider in PROVIDERS) {
75
+ const parsed = parseTuple(rest, provider);
76
+ return parsed ?? { kind: "text", query: raw };
77
+ }
78
+ return { kind: "unknown-provider", provider };
79
+ }
80
+ const tuple = parseTuple(raw, defaultProvider);
81
+ if (tuple) return tuple;
82
+ return { kind: "text", query: raw };
83
+ }
84
+ export {
85
+ PROVIDERS,
86
+ parseLaunch
87
+ };
88
+ //# sourceMappingURL=launch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/launch.ts"],"sourcesContent":["// The launch parser (R3-511; FRONT_DOOR_IA §5.2) — turns what a visitor types\n// into the omnibox into a present route, a typed rejection for an unknown\n// provider, or free text. Pure: no React, no SDK, no network. The site cannot\n// check that a repo EXISTS — existence is the host's job after navigation.\n//\n// Grammar, in the order the parser tries them:\n// 1. a platform URL (any *.immediately.run host with a /present/ or /edit/\n// path) — passed through; the caller resolves it against the current host\n// origin.\n// 2. a provider-prefixed tuple (`github:acme/todo@dev` — the corpus location\n// grammar) or a bare tuple (`acme/todo@feat/x`) with the default provider.\n// 3. a provider URL (`https://github.com/acme/todo/tree/feat/x`).\n// 4. anything else is free text (search only).\n\n/** The known providers: the prefix/spelling a user may type, and the URL host\n * that names the same provider. A second provider is a row here (FRONT_DOOR_IA\n * §5.1 — the chip renders one static option per row). */\nexport const PROVIDERS: Readonly<Record<string, string>> = {\n github: 'github.com',\n};\n\nconst DEFAULT_PROVIDER = 'github';\n\nexport type Launch =\n | {\n kind: 'location';\n provider: string;\n namespace: string;\n repository: string;\n ref?: string;\n /** What the results row shows, e.g. `github:acme/todo@feat/x`. */\n display: string;\n /** A root-relative platform path, e.g. `/present/github/acme/todo/feat%2Fx`. */\n presentPath: string;\n }\n | { kind: 'platform-url'; path: string }\n | { kind: 'unknown-provider'; provider: string }\n | { kind: 'text'; query: string };\n\n/** The `/present/…` path for a location. The ref is encoded ONCE — matching the\n * host's `encodeRef` (site-main `src/editor/shared.ts`), so a ref containing\n * `/` stays one segment. `/files/{entry}` is deliberately never appended: the\n * host resolves the app's entry from `package.json`. */\nfunction presentPathOf(provider: string, namespace: string, repository: string, ref?: string): string {\n const base = `/present/${provider}/${namespace}/${repository}`;\n return ref ? `${base}/${encodeURIComponent(ref)}` : base;\n}\n\nfunction location(provider: string, namespace: string, repository: string, ref?: string): Launch {\n const display = ref\n ? `${provider}:${namespace}/${repository}@${ref}`\n : `${provider}:${namespace}/${repository}`;\n return { kind: 'location', provider, namespace, repository, ...(ref ? { ref } : {}), display, presentPath: presentPathOf(provider, namespace, repository, ref) };\n}\n\n/** `gitlab.com` → `gitlab`: the label we can honestly name for a repo-URL on a\n * host we do not support. Documented shorthand, not a claim about the host. */\nfunction providerLabel(hostname: string): string {\n return hostname.replace(/^www\\./, '').split('.')[0];\n}\n\n/** Parse a `<ns>/<repo>[@<ref>]` tuple. Returns null when the input is not one\n * (wrong shape, missing pieces). The REF may contain `/` (`@feat/x`), so the\n * tail after the namespace is parsed left-to-right, not split on `/`. */\nfunction parseTuple(rest: string, provider: string): Launch | null {\n const slash = rest.indexOf('/');\n if (slash <= 0) return null; // need `<ns>/<repo>`; `acme/` and `acme` are text.\n const namespace = rest.slice(0, slash);\n const tail = rest.slice(slash + 1);\n if (!namespace || !tail) return null;\n const at = tail.indexOf('@');\n const repository = at === -1 ? tail : tail.slice(0, at);\n const ref = at === -1 ? undefined : tail.slice(at + 1);\n if (!repository || repository.includes('/')) return null; // only the ref may span segments\n if (ref !== undefined && !ref) return null; // `ns/repo@` — dangling @.\n return location(provider, namespace, repository, ref);\n}\n\nexport function parseLaunch(input: string, defaultProvider: string = DEFAULT_PROVIDER): Launch {\n const raw = input.trim();\n if (!raw) return { kind: 'text', query: raw };\n\n // 1. URLs.\n if (/^https?:\\/\\//i.test(raw)) {\n let url: URL;\n try {\n url = new URL(raw);\n } catch {\n return { kind: 'text', query: raw };\n }\n const host = url.hostname.toLowerCase();\n\n // This platform: pass the path (with search/hash) through; the caller\n // resolves it against the current host origin.\n if (host === 'immediately.run' || host.endsWith('.immediately.run')) {\n if (/^\\/(present|edit)(\\/|$)/.test(url.pathname)) {\n return { kind: 'platform-url', path: url.pathname + url.search + url.hash };\n }\n return { kind: 'text', query: raw };\n }\n\n const providerEntry = Object.entries(PROVIDERS).find(([, urlHost]) => urlHost === host);\n if (providerEntry) {\n const [provider] = providerEntry;\n const segs = url.pathname.split('/').filter(Boolean);\n if (segs.length < 2) return { kind: 'text', query: raw };\n const [namespace, repo0] = segs;\n const repo = repo0!.endsWith('.git') ? repo0!.slice(0, -4) : repo0!;\n if (!repo) return { kind: 'text', query: raw };\n // `/tree/<rest>` → the ref is the WHOLE remainder (a ref may contain `/`);\n // `/blob/<first>` → the ref is the FIRST segment; the file path is dropped.\n // Documented limitation: a /blob/ URL on a ref containing `/` is misread —\n // the interpreted location is shown in the results row so it can be\n // corrected. Any other extra segment is not a repo root → free text.\n if (segs.length > 2) {\n const [marker, ...extra] = segs.slice(2);\n if (marker === 'tree' && extra.length > 0) {\n return location(provider, namespace, repo, extra.join('/'));\n }\n if (marker === 'blob' && extra.length > 0) {\n return location(provider, namespace, repo, extra[0]);\n }\n return { kind: 'text', query: raw };\n }\n return location(provider, namespace, repo);\n }\n\n // A repo-shaped URL on a host we cannot reach: name what was understood.\n const segs = url.pathname.split('/').filter(Boolean);\n if (segs.length >= 2) return { kind: 'unknown-provider', provider: providerLabel(host) };\n return { kind: 'text', query: raw };\n }\n\n // 2. Provider-prefixed tuple (`github:acme/todo@dev`).\n const colon = raw.indexOf(':');\n if (colon > 0 && !raw.slice(0, colon).includes('/')) {\n const provider = raw.slice(0, colon).toLowerCase();\n const rest = raw.slice(colon + 1);\n if (provider in PROVIDERS) {\n const parsed = parseTuple(rest, provider);\n return parsed ?? { kind: 'text', query: raw };\n }\n return { kind: 'unknown-provider', provider };\n }\n\n // 3. Bare tuple with the default provider.\n const tuple = parseTuple(raw, defaultProvider);\n if (tuple) return tuple;\n\n // 4. Free text — search only.\n return { kind: 'text', query: raw };\n}\n"],"mappings":"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","names":["segs"]}
@@ -0,0 +1,274 @@
1
+ /* The omnibox (R3-512; FRONT_DOOR_IA §5). Tokens come from index.css only; the
2
+ floating results panel (desktop nav variant) is the only shadowed form —
3
+ hero, new, and every mobile form are in-flow and carry no shadow. */
4
+
5
+ .omnibox-outer {
6
+ width: 100%;
7
+ }
8
+
9
+ .omnibox-outer--hero,
10
+ .omnibox-outer--new {
11
+ max-width: 620px;
12
+ }
13
+
14
+ .omnibox-row {
15
+ display: flex;
16
+ align-items: stretch;
17
+ gap: 0;
18
+ border: 1px solid var(--line);
19
+ border-color: var(--accent);
20
+ border-radius: var(--r-md);
21
+ background: var(--panel);
22
+ overflow: hidden;
23
+ }
24
+
25
+ .omnibox-input {
26
+ flex: 1 1 auto;
27
+ min-width: 0;
28
+ border: 0;
29
+ background: transparent;
30
+ color: var(--ink);
31
+ font: 400 16px/1.2 var(--sans);
32
+ padding: 14px 14px;
33
+ outline: none;
34
+ }
35
+
36
+ .omnibox-input:focus-visible {
37
+ outline: none;
38
+ }
39
+
40
+ .omnibox-outer:focus-within .omnibox-row {
41
+ box-shadow: var(--glow);
42
+ }
43
+
44
+ .omnibox-chip {
45
+ display: flex;
46
+ align-items: center;
47
+ padding: 0 0 0 16px;
48
+ flex: 0 0 auto;
49
+ }
50
+
51
+ .omnibox-chip-label,
52
+ .omnibox-chip-select {
53
+ font: 400 12px/1 var(--mono);
54
+ letter-spacing: 0.08em;
55
+ text-transform: uppercase;
56
+ color: var(--ink-2);
57
+ background: transparent;
58
+ border: 0;
59
+ }
60
+
61
+ .omnibox-run {
62
+ flex: 0 0 auto;
63
+ display: inline-flex;
64
+ align-items: center;
65
+ gap: 8px;
66
+ margin: 6px;
67
+ padding: 8px 18px;
68
+ border-radius: var(--r-sm);
69
+ border: 1px solid var(--line);
70
+ background: var(--bg);
71
+ color: var(--ink);
72
+ font: 700 14px/1 var(--sans);
73
+ text-decoration: none;
74
+ cursor: pointer;
75
+ align-self: center;
76
+ }
77
+
78
+ .omnibox-run-arrow {
79
+ font-family: var(--mono);
80
+ }
81
+
82
+ .omnibox--hero .omnibox-run:not([aria-disabled='true']),
83
+ .omnibox--new .omnibox-run:not([aria-disabled='true']) {
84
+ border: 0;
85
+ color: #16101a;
86
+ background-image: var(--grad-btn);
87
+ }
88
+
89
+ .omnibox-run[aria-disabled='true'] {
90
+ opacity: 0.55;
91
+ cursor: default;
92
+ }
93
+
94
+ .omnibox-helper {
95
+ margin: 8px 2px 0;
96
+ font: 400 12px/1.4 var(--mono);
97
+ color: var(--ink-2);
98
+ }
99
+
100
+ /* ── results panel ─────────────────────────────────────────────────────── */
101
+
102
+ .omnibox-panel {
103
+ border: 1px solid var(--line);
104
+ border-radius: var(--r-md);
105
+ background: var(--bg);
106
+ margin-top: 8px;
107
+ padding: 6px;
108
+ /* in-flow by default: it pushes content down (mobile + hero + new) */
109
+ }
110
+
111
+ .omnibox-group {
112
+ padding: 4px 0;
113
+ }
114
+
115
+ .omnibox-group + .omnibox-group {
116
+ border-top: 1px solid var(--line);
117
+ }
118
+
119
+ .omnibox-option {
120
+ display: flex;
121
+ flex-wrap: wrap;
122
+ align-items: baseline;
123
+ gap: 4px 10px;
124
+ padding: 10px 12px;
125
+ border-radius: var(--r-xs);
126
+ color: var(--ink);
127
+ text-decoration: none;
128
+ cursor: pointer;
129
+ }
130
+
131
+ .omnibox-option[aria-selected='true'],
132
+ .omnibox-option:hover {
133
+ background: var(--line);
134
+ }
135
+
136
+ .omnibox-option-name {
137
+ font-weight: 600;
138
+ font-size: 15px;
139
+ }
140
+
141
+ .omnibox-option-cat {
142
+ font: 400 11px/1 var(--mono);
143
+ letter-spacing: 0.08em;
144
+ text-transform: uppercase;
145
+ color: var(--ink-2);
146
+ }
147
+
148
+ .omnibox-option-blurb {
149
+ flex-basis: 100%;
150
+ font-size: 13px;
151
+ color: var(--ink-2);
152
+ }
153
+
154
+ .omnibox-option-action {
155
+ margin-left: auto;
156
+ font: 400 12px/1 var(--mono);
157
+ color: var(--ink-2);
158
+ }
159
+
160
+ .omnibox-empty {
161
+ padding: 14px 12px;
162
+ color: var(--ink-2);
163
+ font-size: 14px;
164
+ }
165
+
166
+ /* ── nav variant (desktop): floating field + shadowed panel ────────────── */
167
+
168
+ .omnibox-outer--nav {
169
+ position: relative;
170
+ width: 230px;
171
+ }
172
+
173
+ .omnibox-outer--nav .omnibox-row {
174
+ border-color: var(--line);
175
+ border-radius: var(--r-sm);
176
+ }
177
+
178
+ .omnibox-outer--nav .omnibox-input {
179
+ padding: 9px 12px;
180
+ font-size: 14px;
181
+ }
182
+
183
+ .omnibox-outer--nav .omnibox-helper {
184
+ display: none;
185
+ }
186
+
187
+ .omnibox-outer--nav .omnibox-panel {
188
+ position: absolute;
189
+ top: calc(100% + 8px);
190
+ left: 0;
191
+ right: 0;
192
+ box-shadow: var(--shadow-pop);
193
+ z-index: 30;
194
+ }
195
+
196
+ .omnibox-nav-button {
197
+ display: inline-flex;
198
+ align-items: center;
199
+ gap: 8px;
200
+ border: 1px solid var(--line);
201
+ border-radius: var(--r-sm);
202
+ background: transparent;
203
+ color: var(--ink);
204
+ font: 400 14px/1 var(--sans);
205
+ padding: 9px 12px;
206
+ cursor: pointer;
207
+ }
208
+
209
+ .omnibox-nav-button .omnibox-nav-button-label {
210
+ color: var(--ink-2);
211
+ }
212
+
213
+ /* ── mobile (FRONT_DOOR_IA §5.5): full width, chip collapses into the field's
214
+ left padding, Run is an in-field 44px arrow, panel in-flow ───────────── */
215
+
216
+ @media (max-width: 720px) {
217
+ .omnibox-outer--nav {
218
+ width: 100%;
219
+ }
220
+
221
+ .omnibox-chip {
222
+ display: none;
223
+ }
224
+
225
+ .omnibox-input {
226
+ padding-left: 16px;
227
+ }
228
+
229
+ .omnibox-run {
230
+ margin: 5px;
231
+ padding: 0;
232
+ width: 44px;
233
+ height: 44px;
234
+ justify-content: center;
235
+ gap: 0;
236
+ }
237
+
238
+ .omnibox-run-label {
239
+ display: none;
240
+ }
241
+
242
+ .omnibox-run-arrow {
243
+ font-size: 20px;
244
+ }
245
+
246
+ .omnibox-outer--nav .omnibox-helper {
247
+ display: block;
248
+ }
249
+
250
+ .omnibox-outer--nav .omnibox-panel {
251
+ position: static;
252
+ box-shadow: none;
253
+ }
254
+ }
255
+
256
+ /* The visually hidden label / live region: present to assistive tech, absent
257
+ to the eye. Scoped here so the omnibox owns its own a11y plumbing. */
258
+ .omnibox-visually-hidden {
259
+ position: absolute;
260
+ width: 1px;
261
+ height: 1px;
262
+ margin: -1px;
263
+ padding: 0;
264
+ overflow: hidden;
265
+ clip: rect(0 0 0 0);
266
+ white-space: nowrap;
267
+ border: 0;
268
+ }
269
+
270
+ /* The keyboard-hint chip the omnibox and the nav shortcut render (R3-512). Moved
271
+ * verbatim from landing-page App.css with the component that renders it — one home,
272
+ * so the Home app renders the same chip. */
273
+ .kbd{font:400 11px var(--mono);background:var(--bg);border:1px solid var(--line);
274
+ border-radius:var(--r-xs);padding:2px 6px;color:var(--ink-3)}
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var omniboxFocus_exports = {};
20
+ __export(omniboxFocus_exports, {
21
+ focusHeroOmnibox: () => focusHeroOmnibox,
22
+ focusOmnibox: () => focusOmnibox,
23
+ registerOmniboxFocus: () => registerOmniboxFocus,
24
+ revealHeroOmnibox: () => revealHeroOmnibox
25
+ });
26
+ module.exports = __toCommonJS(omniboxFocus_exports);
27
+ const stack = [];
28
+ function current(variant) {
29
+ for (let i = stack.length - 1; i >= 0; i -= 1) {
30
+ if (stack[i].variant === variant) return stack[i].handle;
31
+ }
32
+ return void 0;
33
+ }
34
+ function focusHeroOmnibox() {
35
+ current("hero")?.focus();
36
+ }
37
+ function revealHeroOmnibox() {
38
+ const hero = current("hero");
39
+ if (!hero) return;
40
+ hero.reveal?.();
41
+ hero.focus();
42
+ }
43
+ function focusOmnibox() {
44
+ (current("new") ?? current("hero") ?? current("nav"))?.focus();
45
+ }
46
+ function registerOmniboxFocus(variant, handle) {
47
+ const registration = { variant, handle };
48
+ stack.push(registration);
49
+ return () => {
50
+ const at = stack.indexOf(registration);
51
+ if (at !== -1) stack.splice(at, 1);
52
+ };
53
+ }
54
+ // Annotate the CommonJS export names for ESM import in node:
55
+ 0 && (module.exports = {
56
+ focusHeroOmnibox,
57
+ focusOmnibox,
58
+ registerOmniboxFocus,
59
+ revealHeroOmnibox
60
+ });
61
+ //# sourceMappingURL=omniboxFocus.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/omniboxFocus.ts"],"sourcesContent":["// Cross-component focus plumbing for the omnibox. The nav field on `/` activates\n// the hero omnibox, ⌘K focuses whichever omnibox is mounted, and the two \"paste a\n// repo\" buttons further down `/` scroll the hero field into view and focus it.\n// The mounted instances publish their handles here on mount — a module registry,\n// not prop drilling through App. Framework-free by design (Fast refresh: no\n// components in this file).\n//\n// A STACK, not one slot per variant: more than one omnibox of the same variant can\n// be mounted at once (the persistent nav field and the mobile sheet's field are\n// both `nav`), and a single slot made the second instance's UNMOUNT clear the\n// registration the first one still needed — ⌘K then reached nothing. Each\n// registration is removed by identity, so the previous one is restored.\n\nexport type OmniboxVariant = 'hero' | 'nav' | 'new';\n\nexport interface OmniboxHandle {\n /** Move keyboard focus into the field. */\n focus: () => void;\n /** Bring the field into view. Absent for fields that are always on screen. */\n reveal?: () => void;\n}\n\ninterface Registration {\n variant: OmniboxVariant;\n handle: OmniboxHandle;\n}\n\nconst stack: Registration[] = [];\n\n/** The most recently mounted omnibox of this variant, if any is mounted. */\nfunction current(variant: OmniboxVariant): OmniboxHandle | undefined {\n for (let i = stack.length - 1; i >= 0; i -= 1) {\n if (stack[i].variant === variant) return stack[i].handle;\n }\n return undefined;\n}\n\n/** Focus the hero omnibox (no-op when no hero variant is mounted). */\nexport function focusHeroOmnibox(): void {\n current('hero')?.focus();\n}\n\n/** Scroll the hero omnibox into view and focus it — what a \"paste a repo\" CTA\n * further down the page does. No-op when no hero omnibox is mounted. */\nexport function revealHeroOmnibox(): void {\n const hero = current('hero');\n if (!hero) return;\n hero.reveal?.();\n hero.focus();\n}\n\n/** Focus whichever omnibox is mounted — the page's own field first (`new`), then\n * the hero, then the nav field, which is on every route. */\nexport function focusOmnibox(): void {\n (current('new') ?? current('hero') ?? current('nav'))?.focus();\n}\n\n/** Publish a mounted omnibox; the returned function unregisters exactly this one. */\nexport function registerOmniboxFocus(variant: OmniboxVariant, handle: OmniboxHandle): () => void {\n const registration: Registration = { variant, handle };\n stack.push(registration);\n return () => {\n const at = stack.indexOf(registration);\n if (at !== -1) stack.splice(at, 1);\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA2BA,MAAM,QAAwB,CAAC;AAG/B,SAAS,QAAQ,SAAoD;AACnE,WAAS,IAAI,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK,GAAG;AAC7C,QAAI,MAAM,CAAC,EAAE,YAAY,QAAS,QAAO,MAAM,CAAC,EAAE;AAAA,EACpD;AACA,SAAO;AACT;AAGO,SAAS,mBAAyB;AACvC,UAAQ,MAAM,GAAG,MAAM;AACzB;AAIO,SAAS,oBAA0B;AACxC,QAAM,OAAO,QAAQ,MAAM;AAC3B,MAAI,CAAC,KAAM;AACX,OAAK,SAAS;AACd,OAAK,MAAM;AACb;AAIO,SAAS,eAAqB;AACnC,GAAC,QAAQ,KAAK,KAAK,QAAQ,MAAM,KAAK,QAAQ,KAAK,IAAI,MAAM;AAC/D;AAGO,SAAS,qBAAqB,SAAyB,QAAmC;AAC/F,QAAM,eAA6B,EAAE,SAAS,OAAO;AACrD,QAAM,KAAK,YAAY;AACvB,SAAO,MAAM;AACX,UAAM,KAAK,MAAM,QAAQ,YAAY;AACrC,QAAI,OAAO,GAAI,OAAM,OAAO,IAAI,CAAC;AAAA,EACnC;AACF;","names":[]}
@@ -0,0 +1,19 @@
1
+ type OmniboxVariant = 'hero' | 'nav' | 'new';
2
+ interface OmniboxHandle {
3
+ /** Move keyboard focus into the field. */
4
+ focus: () => void;
5
+ /** Bring the field into view. Absent for fields that are always on screen. */
6
+ reveal?: () => void;
7
+ }
8
+ /** Focus the hero omnibox (no-op when no hero variant is mounted). */
9
+ declare function focusHeroOmnibox(): void;
10
+ /** Scroll the hero omnibox into view and focus it — what a "paste a repo" CTA
11
+ * further down the page does. No-op when no hero omnibox is mounted. */
12
+ declare function revealHeroOmnibox(): void;
13
+ /** Focus whichever omnibox is mounted — the page's own field first (`new`), then
14
+ * the hero, then the nav field, which is on every route. */
15
+ declare function focusOmnibox(): void;
16
+ /** Publish a mounted omnibox; the returned function unregisters exactly this one. */
17
+ declare function registerOmniboxFocus(variant: OmniboxVariant, handle: OmniboxHandle): () => void;
18
+
19
+ export { type OmniboxHandle, type OmniboxVariant, focusHeroOmnibox, focusOmnibox, registerOmniboxFocus, revealHeroOmnibox };
@@ -0,0 +1,19 @@
1
+ type OmniboxVariant = 'hero' | 'nav' | 'new';
2
+ interface OmniboxHandle {
3
+ /** Move keyboard focus into the field. */
4
+ focus: () => void;
5
+ /** Bring the field into view. Absent for fields that are always on screen. */
6
+ reveal?: () => void;
7
+ }
8
+ /** Focus the hero omnibox (no-op when no hero variant is mounted). */
9
+ declare function focusHeroOmnibox(): void;
10
+ /** Scroll the hero omnibox into view and focus it — what a "paste a repo" CTA
11
+ * further down the page does. No-op when no hero omnibox is mounted. */
12
+ declare function revealHeroOmnibox(): void;
13
+ /** Focus whichever omnibox is mounted — the page's own field first (`new`), then
14
+ * the hero, then the nav field, which is on every route. */
15
+ declare function focusOmnibox(): void;
16
+ /** Publish a mounted omnibox; the returned function unregisters exactly this one. */
17
+ declare function registerOmniboxFocus(variant: OmniboxVariant, handle: OmniboxHandle): () => void;
18
+
19
+ export { type OmniboxHandle, type OmniboxVariant, focusHeroOmnibox, focusOmnibox, registerOmniboxFocus, revealHeroOmnibox };
@@ -0,0 +1,34 @@
1
+ const stack = [];
2
+ function current(variant) {
3
+ for (let i = stack.length - 1; i >= 0; i -= 1) {
4
+ if (stack[i].variant === variant) return stack[i].handle;
5
+ }
6
+ return void 0;
7
+ }
8
+ function focusHeroOmnibox() {
9
+ current("hero")?.focus();
10
+ }
11
+ function revealHeroOmnibox() {
12
+ const hero = current("hero");
13
+ if (!hero) return;
14
+ hero.reveal?.();
15
+ hero.focus();
16
+ }
17
+ function focusOmnibox() {
18
+ (current("new") ?? current("hero") ?? current("nav"))?.focus();
19
+ }
20
+ function registerOmniboxFocus(variant, handle) {
21
+ const registration = { variant, handle };
22
+ stack.push(registration);
23
+ return () => {
24
+ const at = stack.indexOf(registration);
25
+ if (at !== -1) stack.splice(at, 1);
26
+ };
27
+ }
28
+ export {
29
+ focusHeroOmnibox,
30
+ focusOmnibox,
31
+ registerOmniboxFocus,
32
+ revealHeroOmnibox
33
+ };
34
+ //# sourceMappingURL=omniboxFocus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/omniboxFocus.ts"],"sourcesContent":["// Cross-component focus plumbing for the omnibox. The nav field on `/` activates\n// the hero omnibox, ⌘K focuses whichever omnibox is mounted, and the two \"paste a\n// repo\" buttons further down `/` scroll the hero field into view and focus it.\n// The mounted instances publish their handles here on mount — a module registry,\n// not prop drilling through App. Framework-free by design (Fast refresh: no\n// components in this file).\n//\n// A STACK, not one slot per variant: more than one omnibox of the same variant can\n// be mounted at once (the persistent nav field and the mobile sheet's field are\n// both `nav`), and a single slot made the second instance's UNMOUNT clear the\n// registration the first one still needed — ⌘K then reached nothing. Each\n// registration is removed by identity, so the previous one is restored.\n\nexport type OmniboxVariant = 'hero' | 'nav' | 'new';\n\nexport interface OmniboxHandle {\n /** Move keyboard focus into the field. */\n focus: () => void;\n /** Bring the field into view. Absent for fields that are always on screen. */\n reveal?: () => void;\n}\n\ninterface Registration {\n variant: OmniboxVariant;\n handle: OmniboxHandle;\n}\n\nconst stack: Registration[] = [];\n\n/** The most recently mounted omnibox of this variant, if any is mounted. */\nfunction current(variant: OmniboxVariant): OmniboxHandle | undefined {\n for (let i = stack.length - 1; i >= 0; i -= 1) {\n if (stack[i].variant === variant) return stack[i].handle;\n }\n return undefined;\n}\n\n/** Focus the hero omnibox (no-op when no hero variant is mounted). */\nexport function focusHeroOmnibox(): void {\n current('hero')?.focus();\n}\n\n/** Scroll the hero omnibox into view and focus it — what a \"paste a repo\" CTA\n * further down the page does. No-op when no hero omnibox is mounted. */\nexport function revealHeroOmnibox(): void {\n const hero = current('hero');\n if (!hero) return;\n hero.reveal?.();\n hero.focus();\n}\n\n/** Focus whichever omnibox is mounted — the page's own field first (`new`), then\n * the hero, then the nav field, which is on every route. */\nexport function focusOmnibox(): void {\n (current('new') ?? current('hero') ?? current('nav'))?.focus();\n}\n\n/** Publish a mounted omnibox; the returned function unregisters exactly this one. */\nexport function registerOmniboxFocus(variant: OmniboxVariant, handle: OmniboxHandle): () => void {\n const registration: Registration = { variant, handle };\n stack.push(registration);\n return () => {\n const at = stack.indexOf(registration);\n if (at !== -1) stack.splice(at, 1);\n };\n}\n"],"mappings":"AA2BA,MAAM,QAAwB,CAAC;AAG/B,SAAS,QAAQ,SAAoD;AACnE,WAAS,IAAI,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK,GAAG;AAC7C,QAAI,MAAM,CAAC,EAAE,YAAY,QAAS,QAAO,MAAM,CAAC,EAAE;AAAA,EACpD;AACA,SAAO;AACT;AAGO,SAAS,mBAAyB;AACvC,UAAQ,MAAM,GAAG,MAAM;AACzB;AAIO,SAAS,oBAA0B;AACxC,QAAM,OAAO,QAAQ,MAAM;AAC3B,MAAI,CAAC,KAAM;AACX,OAAK,SAAS;AACd,OAAK,MAAM;AACb;AAIO,SAAS,eAAqB;AACnC,GAAC,QAAQ,KAAK,KAAK,QAAQ,MAAM,KAAK,QAAQ,KAAK,IAAI,MAAM;AAC/D;AAGO,SAAS,qBAAqB,SAAyB,QAAmC;AAC/F,QAAM,eAA6B,EAAE,SAAS,OAAO;AACrD,QAAM,KAAK,YAAY;AACvB,SAAO,MAAM;AACX,UAAM,KAAK,MAAM,QAAQ,YAAY;AACrC,QAAI,OAAO,GAAI,OAAM,OAAO,IAAI,CAAC;AAAA,EACnC;AACF;","names":[]}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@immediately-run/omnibox",
3
+ "version": "0.1.0",
4
+ "description": "The front door's omnibox: one list-autocomplete combobox that runs a repo by URL or tuple and searches a consumer's app directory and docs (the launch grammar always; the hit sources injected). Shared by landing-page and the Home app so there is one component and one grammar (R3-512, R3-530).",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/immediately-run/omnibox.git"
9
+ },
10
+ "type": "module",
11
+ "main": "./dist/index.cjs",
12
+ "module": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js",
18
+ "require": "./dist/index.cjs"
19
+ },
20
+ "./omnibox.css": "./dist/omnibox.css"
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "sideEffects": [
26
+ "*.css"
27
+ ],
28
+ "scripts": {
29
+ "build": "tsup && node scripts/build-css.mjs",
30
+ "test": "vitest run",
31
+ "lint": "eslint src",
32
+ "prepare": "npm run build",
33
+ "check:pins": "node scripts/check-dependency-pins.mjs --self-test && node scripts/check-dependency-pins.mjs",
34
+ "check:publish-version": "node scripts/check-publish-version.mjs --self-test && node scripts/check-publish-version.mjs"
35
+ },
36
+ "peerDependencies": {
37
+ "@immediately-run/sdk": "0.60.0",
38
+ "react": "^19.2.5"
39
+ },
40
+ "devDependencies": {
41
+ "@eslint/js": "^9.18.0",
42
+ "@immediately-run/sdk": "0.60.0",
43
+ "@testing-library/react": "^16.1.0",
44
+ "@testing-library/user-event": "^14.6.7",
45
+ "@types/react": "^19.2.14",
46
+ "@types/react-dom": "^19.2.3",
47
+ "eslint": "^9.18.0",
48
+ "jsdom": "^25.0.1",
49
+ "react": "^19.2.5",
50
+ "react-dom": "^19.2.5",
51
+ "tsup": "^8.0.0",
52
+ "typescript": "^5.9.3",
53
+ "typescript-eslint": "^8.20.0",
54
+ "vitest": "^4.1.11"
55
+ }
56
+ }