quiver-cli 0.6.0 → 0.8.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/LICENSE +21 -0
- package/dist/cli.js +314 -86
- package/package.json +1 -1
- package/template/.agents/skills/agent-browser/SKILL.md +4 -9
- package/template/.agents/skills/apps/skybridge/SKILL.md +1 -1
- package/template/.agents/skills/code/improve/SKILL.md +9 -5
- package/template/.agents/skills/code/improve/references/audit-playbook.md +10 -10
- package/template/.agents/skills/code/improve/references/closing-the-loop.md +4 -3
- package/template/.agents/skills/code/improve/references/plan-template.md +5 -0
- package/template/.agents/skills/design/impeccable/SKILL.md +14 -24
- package/template/.agents/skills/design/impeccable/reference/animate.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/bolder.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/brand.md +2 -2
- package/template/.agents/skills/design/impeccable/reference/colorize.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/critique.md +6 -6
- package/template/.agents/skills/design/impeccable/reference/delight.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/distill.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/document.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/extract.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/hooks.md +90 -0
- package/template/.agents/skills/design/impeccable/reference/init.md +5 -5
- package/template/.agents/skills/design/impeccable/reference/live.md +16 -16
- package/template/.agents/skills/design/impeccable/reference/overdrive.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/polish.md +2 -2
- package/template/.agents/skills/design/impeccable/reference/quieter.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/shape.md +2 -2
- package/template/.agents/skills/design/impeccable/scripts/context-signals.mjs +1 -1
- package/template/.agents/skills/design/impeccable/scripts/context.mjs +724 -33
- package/template/.agents/skills/design/impeccable/scripts/critique-storage.mjs +1 -1
- package/template/.agents/skills/design/impeccable/scripts/detector/browser/injected/index.mjs +204 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/cli/main.mjs +57 -11
- package/template/.agents/skills/design/impeccable/scripts/detector/design-system.mjs +750 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns-browser.js +633 -46
- package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns.mjs +7 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/browser/detect-url.mjs +29 -4
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/regex/detect-text.mjs +43 -10
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +29 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/detect-html.mjs +27 -1
- package/template/.agents/skills/design/impeccable/scripts/detector/node/file-system.mjs +1 -1
- package/template/.agents/skills/design/impeccable/scripts/detector/registry/antipatterns.mjs +29 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/rules/checks.mjs +401 -46
- package/template/.agents/skills/design/impeccable/scripts/detector/shared/inline-ignores.mjs +148 -0
- package/template/.agents/skills/design/impeccable/scripts/hook-admin.mjs +661 -0
- package/template/.agents/skills/design/impeccable/scripts/hook-before-edit.mjs +476 -0
- package/template/.agents/skills/design/impeccable/scripts/hook-lib.mjs +1632 -0
- package/template/.agents/skills/design/impeccable/scripts/hook.mjs +61 -0
- package/template/.agents/skills/design/impeccable/scripts/{design-parser.mjs → lib/design-parser.mjs} +8 -1
- package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-config.mjs +638 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-paths.mjs +128 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/target-args.mjs +42 -0
- package/template/.agents/skills/design/impeccable/scripts/live/browser-script-parts.mjs +49 -0
- package/template/.agents/skills/design/impeccable/scripts/{live-event-validation.mjs → live/event-validation.mjs} +6 -5
- package/template/.agents/skills/design/impeccable/scripts/live/manual-apply.mjs +939 -0
- package/template/.agents/skills/design/impeccable/scripts/live/manual-edit-routes.mjs +357 -0
- package/template/.agents/skills/design/impeccable/scripts/{live-manual-edits-buffer.mjs → live/manual-edits-buffer.mjs} +1 -1
- package/template/.agents/skills/design/impeccable/scripts/{live-session-store.mjs → live/session-store.mjs} +1 -1
- package/template/.agents/skills/design/impeccable/scripts/{live-ui-core.mjs → live/ui-core.mjs} +2 -1
- package/template/.agents/skills/design/impeccable/scripts/live/vocabulary.mjs +36 -0
- package/template/.agents/skills/design/impeccable/scripts/live-accept.mjs +3 -3
- package/template/.agents/skills/design/impeccable/scripts/live-browser-dom.js +146 -0
- package/template/.agents/skills/design/impeccable/scripts/live-browser.js +1456 -599
- package/template/.agents/skills/design/impeccable/scripts/live-commit-manual-edits.mjs +2 -2
- package/template/.agents/skills/design/impeccable/scripts/live-complete.mjs +2 -2
- package/template/.agents/skills/design/impeccable/scripts/live-discard-manual-edits.mjs +1 -1
- package/template/.agents/skills/design/impeccable/scripts/live-inject.mjs +35 -9
- package/template/.agents/skills/design/impeccable/scripts/live-insert.mjs +2 -2
- package/template/.agents/skills/design/impeccable/scripts/live-manual-edit-evidence.mjs +2 -2
- package/template/.agents/skills/design/impeccable/scripts/live-poll.mjs +18 -13
- package/template/.agents/skills/design/impeccable/scripts/live-resume.mjs +1 -1
- package/template/.agents/skills/design/impeccable/scripts/live-server.mjs +77 -1264
- package/template/.agents/skills/design/impeccable/scripts/live-status.mjs +2 -2
- package/template/.agents/skills/design/impeccable/scripts/live-target.mjs +30 -0
- package/template/.agents/skills/design/impeccable/scripts/live-wrap.mjs +4 -4
- package/template/.agents/skills/design/impeccable/scripts/live.mjs +73 -22
- package/template/.agents/skills/writing/humanizer/SKILL.md +621 -0
- package/template/.agents/upstreams.json +17 -8
- package/template/.agents/skills/apps/skybridge/references/architecture.md +0 -175
- package/template/.agents/skills/apps/skybridge/references/copy-template.md +0 -24
- package/template/.agents/skills/apps/skybridge/references/csp.md +0 -33
- package/template/.agents/skills/apps/skybridge/references/deploy.md +0 -33
- package/template/.agents/skills/apps/skybridge/references/discover.md +0 -84
- package/template/.agents/skills/apps/skybridge/references/download-file.md +0 -77
- package/template/.agents/skills/apps/skybridge/references/fetch-and-render-data.md +0 -151
- package/template/.agents/skills/apps/skybridge/references/oauth.md +0 -115
- package/template/.agents/skills/apps/skybridge/references/open-external-links.md +0 -71
- package/template/.agents/skills/apps/skybridge/references/prompt-llm.md +0 -20
- package/template/.agents/skills/apps/skybridge/references/publish.md +0 -19
- package/template/.agents/skills/apps/skybridge/references/run-locally.md +0 -51
- package/template/.agents/skills/apps/skybridge/references/state-and-context.md +0 -151
- package/template/.agents/skills/apps/skybridge/references/ui-guidelines.md +0 -205
- package/template/.agents/skills/design/impeccable/scripts/cleanup-deprecated.mjs +0 -284
- package/template/.agents/skills/design/impeccable/scripts/impeccable-paths.mjs +0 -126
- /package/template/.agents/skills/design/impeccable/scripts/{is-generated.mjs → lib/is-generated.mjs} +0 -0
- /package/template/.agents/skills/design/impeccable/scripts/{live-completion.mjs → live/completion.mjs} +0 -0
- /package/template/.agents/skills/design/impeccable/scripts/{live-insert-ui.mjs → live/insert-ui.mjs} +0 -0
- /package/template/.agents/skills/design/impeccable/scripts/{live-svelte-component.mjs → live/svelte-component.mjs} +0 -0
- /package/template/.agents/skills/design/impeccable/scripts/{live-sveltekit-adapter.mjs → live/sveltekit-adapter.mjs} +0 -0
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
# Open external links
|
|
2
|
-
|
|
3
|
-
- "Open in App" button URL → `useSetOpenInAppUrl`
|
|
4
|
-
- External redirect → `useOpenExternal`
|
|
5
|
-
|
|
6
|
-
## "Open in App" button
|
|
7
|
-
|
|
8
|
-
Top right corner in fullscreen mode. Set programmatically. If the origin matches the view server URL, ChatGPT navigates to the full href (any path). If the origin differs, ChatGPT falls back to the view server URL.
|
|
9
|
-
|
|
10
|
-
**Example**:
|
|
11
|
-
```tsx
|
|
12
|
-
import { useSetOpenInAppUrl } from "skybridge/web";
|
|
13
|
-
import { useEffect } from "react";
|
|
14
|
-
|
|
15
|
-
function ProductDetail({ productId }: { productId: string }) {
|
|
16
|
-
const setOpenInAppUrl = useSetOpenInAppUrl();
|
|
17
|
-
|
|
18
|
-
useEffect(() => {
|
|
19
|
-
setOpenInAppUrl(`https://example.com/products/${productId}`).catch(console.error);
|
|
20
|
-
}, [productId]);
|
|
21
|
-
|
|
22
|
-
return <div>{/* Product details */}</div>;
|
|
23
|
-
}
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## External redirect
|
|
27
|
-
|
|
28
|
-
**Example**:
|
|
29
|
-
```tsx
|
|
30
|
-
import { useOpenExternal } from "skybridge/web";
|
|
31
|
-
|
|
32
|
-
function ExternalLink() {
|
|
33
|
-
const openExternal = useOpenExternal();
|
|
34
|
-
|
|
35
|
-
return (
|
|
36
|
-
<button onClick={() => openExternal("https://example.com")}>
|
|
37
|
-
Visit Website
|
|
38
|
-
</button>
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
You can control return-path behavior with an optional second argument (ChatGPT only):
|
|
45
|
-
|
|
46
|
-
```tsx
|
|
47
|
-
openExternal("https://example.com", { redirectUrl: false });
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
Use `redirectUrl: false` to skip automatic `?redirectUrl=...` appending.
|
|
51
|
-
|
|
52
|
-
Shows confirmation dialog unless domain is whitelisted:
|
|
53
|
-
|
|
54
|
-
```typescript
|
|
55
|
-
// src/server.ts
|
|
56
|
-
server.registerTool(
|
|
57
|
-
{
|
|
58
|
-
name: "search-flights",
|
|
59
|
-
description: "Search for flights",
|
|
60
|
-
inputSchema: { destination: z.string(), dates: z.string() },
|
|
61
|
-
view: {
|
|
62
|
-
component: "search-flights",
|
|
63
|
-
description: "Flight results",
|
|
64
|
-
csp: {
|
|
65
|
-
redirectDomains: ["https://airline.example.com"],
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
},
|
|
69
|
-
async ({ destination, dates }) => { /* ... */ }
|
|
70
|
-
);
|
|
71
|
-
```
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# Prompt model
|
|
2
|
-
|
|
3
|
-
Trigger an LLM completion from user interaction with `useSendFollowUpMessage`.
|
|
4
|
-
|
|
5
|
-
**Example:**
|
|
6
|
-
```tsx
|
|
7
|
-
import { useSendFollowUpMessage } from "skybridge/web";
|
|
8
|
-
|
|
9
|
-
export function FindBestFlightButton() {
|
|
10
|
-
const sendMessage = useSendFollowUpMessage();
|
|
11
|
-
|
|
12
|
-
return (
|
|
13
|
-
<button onClick={() => sendMessage({
|
|
14
|
-
prompt: "Find the best flight option, based on user preferences and agenda."
|
|
15
|
-
})}>
|
|
16
|
-
Find Best Flight
|
|
17
|
-
</button>
|
|
18
|
-
);
|
|
19
|
-
}
|
|
20
|
-
```
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# Publish to Directories
|
|
2
|
-
|
|
3
|
-
## 1. Audit Annotations
|
|
4
|
-
|
|
5
|
-
**Common cause of rejection.** Ensure all tools and views have correct annotations. See [fetch-and-render-data.md](fetch-and-render-data.md).
|
|
6
|
-
|
|
7
|
-
## 2. Audit CSP
|
|
8
|
-
|
|
9
|
-
Ensure all external domains are declared in the tool's `view.csp`. See [csp.md](csp.md).
|
|
10
|
-
|
|
11
|
-
## 3. Submit
|
|
12
|
-
|
|
13
|
-
### ChatGPT
|
|
14
|
-
Guide user to submit the app at [platform.openai.com](https://platform.openai.com) → Apps.
|
|
15
|
-
|
|
16
|
-
OpenAI verifies app ownership via `/.well-known/openai-apps-challenge`. Guide user to Alpic **Distribution** tab → **OpenAI Apps Verification Token** → paste the token from OpenAI.
|
|
17
|
-
|
|
18
|
-
### Claude
|
|
19
|
-
Guide user to submit the app on the [Anthropic Connectors Directory FAQ](https://support.claude.com/en/articles/11596036-anthropic-connectors-directory-faq).
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# Running Locally Workflow
|
|
2
|
-
|
|
3
|
-
## 1. Start Dev Server
|
|
4
|
-
|
|
5
|
-
Install dependencies and start the dev server in the background:
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
{pm} install && {pm} run dev
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
For Deno projects, use `deno task dev` instead.
|
|
12
|
-
|
|
13
|
-
When started, output the local MCP server and devtools URL.
|
|
14
|
-
|
|
15
|
-
Hot reload enabled (nodemon for server, HMR for views).
|
|
16
|
-
|
|
17
|
-
## 2. Connect to AI Assistants (Optional)
|
|
18
|
-
|
|
19
|
-
Ask user if they want to test in ChatGPT/Claude or just use local devtools.
|
|
20
|
-
|
|
21
|
-
If yes, expose the local server via Alpic tunnel:
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
alpic tunnel --port 3000
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
Extract the forwarding URL from Alpic tunnel output (e.g., `https://cool-marmot-fondue-420.alpic.dev`).
|
|
28
|
-
|
|
29
|
-
### Connect to ChatGPT
|
|
30
|
-
Provide the user with these instructions to create the app in ChatGPT:
|
|
31
|
-
1. Go to [Apps Settings](https://chatgpt.com/apps#settings/Connectors) → Create App
|
|
32
|
-
2. Enter a name and description for the app
|
|
33
|
-
3. Paste this URL: `{tunnel-url}/mcp`
|
|
34
|
-
4. Set the appropriate Authentication scheme. In doubt, pick "No Authentication"
|
|
35
|
-
5. Click Create
|
|
36
|
-
6. Test by typing `@{app-name}` in a ChatGPT chat
|
|
37
|
-
|
|
38
|
-
**Troubleshooting:**
|
|
39
|
-
- 'Create App' button missing: ask user to enable Developer mode in Settings → Apps → Advanced Settings
|
|
40
|
-
- 'Create App' button not working: confirm they have ChatGPT Plus, Pro, Business, or Enterprise/Edu plan
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
### Connect to Claude
|
|
44
|
-
Provide the user with these instructions to create the app in Claude:
|
|
45
|
-
1. Go to [Connector Settings](https://claude.ai/settings/connectors) → Add Custom Connector
|
|
46
|
-
2. Enter a name and URL: `{tunnel-url}/mcp`
|
|
47
|
-
3. Click Create
|
|
48
|
-
4. In Claude chat, click the `+` button and select `@{app-name}`
|
|
49
|
-
|
|
50
|
-
**Troubleshooting:**
|
|
51
|
-
- 'Add Custom Connector' button missing: confirm they have a Claude paid plan
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
# Manage View State and LLM Context
|
|
2
|
-
|
|
3
|
-
- View state (`useViewState`/`createStore`) persists and is visible to LLM as structured data.
|
|
4
|
-
- `data-llm` gives LLM context for referential language ("this one").
|
|
5
|
-
- React `useState` is ephemeral and invisible to LLM.
|
|
6
|
-
|
|
7
|
-
**Decision guide:**
|
|
8
|
-
| Need | Use |
|
|
9
|
-
|------|-----|
|
|
10
|
-
| Persist data, single component | `useViewState` |
|
|
11
|
-
| Persist data, shared across components, complex mutations | `createStore` |
|
|
12
|
-
| Help LLM understand "this one" | `data-llm` |
|
|
13
|
-
| Ephemeral UI only (hover, animation) | `useState` |
|
|
14
|
-
|
|
15
|
-
## useViewState
|
|
16
|
-
|
|
17
|
-
Single component, simple access patterns.
|
|
18
|
-
|
|
19
|
-
```tsx
|
|
20
|
-
function SeatPicker({ seats }) {
|
|
21
|
-
const [{ selectedSeat }, setState] = useViewState({ selectedSeat: null });
|
|
22
|
-
|
|
23
|
-
return (
|
|
24
|
-
<div className="seat-grid">
|
|
25
|
-
{seats.map(seat => (
|
|
26
|
-
<button
|
|
27
|
-
key={seat.id}
|
|
28
|
-
onClick={() => setState((prev) => ({ ...prev, selectedSeat: seat.id }))}
|
|
29
|
-
className={selectedSeat === seat.id ? "selected" : ""}
|
|
30
|
-
>
|
|
31
|
-
{seat.id}
|
|
32
|
-
</button>
|
|
33
|
-
))}
|
|
34
|
-
</div>
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
**Why useViewState:** Single component reads `selectedSeat` to highlight button. View or LLM reads when booking.
|
|
40
|
-
|
|
41
|
-
## createStore
|
|
42
|
-
|
|
43
|
-
Shared across components, complex mutations. `createStore` is a thin wrapper around Zustand.
|
|
44
|
-
|
|
45
|
-
```tsx
|
|
46
|
-
import { createStore } from "skybridge/web";
|
|
47
|
-
|
|
48
|
-
const useCartStore = createStore<CartState>((set) => ({
|
|
49
|
-
cart: [],
|
|
50
|
-
add: (item) => set((s) => ({ cart: [...s.cart, item] })),
|
|
51
|
-
remove: (id) => set((s) => ({ cart: s.cart.filter(i => i.id !== id) })),
|
|
52
|
-
}));
|
|
53
|
-
|
|
54
|
-
// ProductCard.tsx
|
|
55
|
-
function ProductCard({ product }) {
|
|
56
|
-
const add = useCartStore((s) => s.add);
|
|
57
|
-
return <button onClick={() => add(product)}>Add to Cart</button>;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// CartSummary.tsx
|
|
61
|
-
function CartSummary() {
|
|
62
|
-
const cart = useCartStore((s) => s.cart);
|
|
63
|
-
return <span>{cart.length} items</span>;
|
|
64
|
-
}
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
**Why createStore:** Cart accessed by multiple components. View or LLM reads items at checkout.
|
|
68
|
-
|
|
69
|
-
## data-llm
|
|
70
|
-
|
|
71
|
-
Tell the LLM what user is viewing/doing. One-way—view doesn't read it back. These are annotations—don't put complex objects here.
|
|
72
|
-
|
|
73
|
-
```tsx
|
|
74
|
-
function ProductDetail({ product }) {
|
|
75
|
-
return (
|
|
76
|
-
<div data-llm={`Viewing: ${product.name}, $${product.price}, ${product.inStock ? "in stock" : "out of stock"}`}>
|
|
77
|
-
<h1>{product.name}</h1>
|
|
78
|
-
<p>${product.price}</p>
|
|
79
|
-
</div>
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
**Why data-llm:** When user asks "Is this one good?" or "Add this to cart", LLM knows what "this" refers to.
|
|
85
|
-
|
|
86
|
-
## Common mistakes
|
|
87
|
-
|
|
88
|
-
```tsx
|
|
89
|
-
// DON'T: useState is not persisted, LLM can't see it
|
|
90
|
-
const [selected, setSelected] = useState(null);
|
|
91
|
-
|
|
92
|
-
// DO: useViewState persists and LLM sees it
|
|
93
|
-
const [{ selected }, setState] = useViewState({ selected: null });
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
```tsx
|
|
97
|
-
// DON'T: Complex object in data-llm
|
|
98
|
-
<div data-llm={JSON.stringify(cart)}>
|
|
99
|
-
|
|
100
|
-
// DO: Human-readable summary
|
|
101
|
-
<div data-llm={`Cart: ${cart.length} items, $${total}`}>
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
## Combined example
|
|
105
|
-
|
|
106
|
-
Todo list. User checks off tasks, asks "what should I prioritize?"
|
|
107
|
-
|
|
108
|
-
```tsx
|
|
109
|
-
function TaskList() {
|
|
110
|
-
// PERSIST: All tasks with completed status
|
|
111
|
-
const [{ tasks }, setState] = useViewState({
|
|
112
|
-
tasks: [
|
|
113
|
-
{ id: 1, title: "Buy groceries", completed: false },
|
|
114
|
-
{ id: 2, title: "Call mom", completed: true },
|
|
115
|
-
]
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
// EPHEMERAL: Task user is viewing — reset on reopen
|
|
119
|
-
const [viewing, setViewing] = useState(null);
|
|
120
|
-
|
|
121
|
-
return (
|
|
122
|
-
// CONTEXT: What user is looking at — LLM answers "how should I handle this task?"
|
|
123
|
-
<div data-llm={viewing
|
|
124
|
-
? `Viewing: "${viewing.title}"`
|
|
125
|
-
: `${tasks.filter(t => !t.completed).length} tasks remaining`
|
|
126
|
-
}>
|
|
127
|
-
{tasks.map(t => (
|
|
128
|
-
<Task
|
|
129
|
-
key={t.id}
|
|
130
|
-
task={t}
|
|
131
|
-
onView={() => setViewing(t)}
|
|
132
|
-
onToggle={() => setState((prev) => ({
|
|
133
|
-
...prev,
|
|
134
|
-
tasks: prev.tasks.map(task =>
|
|
135
|
-
task.id === t.id ? { ...task, completed: !task.completed } : task
|
|
136
|
-
)
|
|
137
|
-
}))}
|
|
138
|
-
/>
|
|
139
|
-
))}
|
|
140
|
-
</div>
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
**Why each?**
|
|
146
|
-
|
|
147
|
-
| What | API | Why |
|
|
148
|
-
|------|-----|-----|
|
|
149
|
-
| `tasks` | `useViewState` | Persists. Tasks and progress survive reopen. |
|
|
150
|
-
| `viewing` | `useState` | Ephemeral. Current focus resets on reopen. |
|
|
151
|
-
| `"Viewing: Buy groceries"` | `data-llm` | LLM context. Understands "this task" in conversation. |
|
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
# UI Guidelines
|
|
2
|
-
|
|
3
|
-
## Contents
|
|
4
|
-
- [Display Modes](#display-modes) — inline, fullscreen, PiP, switching
|
|
5
|
-
- [Modal](#modal) — overlay on top of any display mode
|
|
6
|
-
- [Adapting to Host](#adapting-to-host) — layout constraints, theme
|
|
7
|
-
- [Adapting to User](#adapting-to-user) — device, locale
|
|
8
|
-
|
|
9
|
-
## Display Modes
|
|
10
|
-
|
|
11
|
-
Views render **inline by default**. Add fullscreen and/or PiP when the use case benefits from it—implement triggers (button, gesture) to let users switch.
|
|
12
|
-
|
|
13
|
-
### Inline (default)
|
|
14
|
-
|
|
15
|
-
View appears embedded in conversation above the model response.
|
|
16
|
-
|
|
17
|
-
**Use for:** Single result display, quick actions, browsing items.
|
|
18
|
-
|
|
19
|
-
**Constraints:**
|
|
20
|
-
- Max 2 CTAs (one primary, one secondary)
|
|
21
|
-
- No `overflow: scroll/auto`—content must fit within available space
|
|
22
|
-
- No tabs or deep navigation
|
|
23
|
-
|
|
24
|
-
**Patterns:**
|
|
25
|
-
- **Card** — Single-purpose view (order confirmation, weather, status)
|
|
26
|
-
- **Carousel** — 3-8 browsable items with image + title + max 3 lines metadata
|
|
27
|
-
|
|
28
|
-
### Fullscreen
|
|
29
|
-
|
|
30
|
-
Immersive experience for complex tasks. Host composer remains overlaid at bottom.
|
|
31
|
-
|
|
32
|
-
**Use for:** Multi-step workflows, rich editing, explorable content, detailed comparisons.
|
|
33
|
-
|
|
34
|
-
**Constraints:**
|
|
35
|
-
- Composer overlay always visible at bottom
|
|
36
|
-
- User can still chat while in fullscreen
|
|
37
|
-
|
|
38
|
-
### Picture-in-Picture (PiP)
|
|
39
|
-
|
|
40
|
-
Persistent floating window that stays visible during conversation.
|
|
41
|
-
|
|
42
|
-
**Use for:** Live sessions (timers, streams), games, real-time status.
|
|
43
|
-
|
|
44
|
-
**Constraints:**
|
|
45
|
-
- Must update/respond to user interaction—don't use for static content
|
|
46
|
-
- Minimal controls—this is a glanceable surface
|
|
47
|
-
- On mobile, PiP coerces to fullscreen
|
|
48
|
-
|
|
49
|
-
### Switching Modes
|
|
50
|
-
|
|
51
|
-
Use `useDisplayMode` to read current mode and request changes.
|
|
52
|
-
|
|
53
|
-
**Constraints:**
|
|
54
|
-
- User-triggered only—never switch programmatically
|
|
55
|
-
- Host may reject the request
|
|
56
|
-
|
|
57
|
-
```tsx
|
|
58
|
-
import { useDisplayMode } from "skybridge/web";
|
|
59
|
-
|
|
60
|
-
function ExpandableView() {
|
|
61
|
-
const [displayMode, setDisplayMode] = useDisplayMode();
|
|
62
|
-
const isFullscreen = displayMode === "fullscreen";
|
|
63
|
-
|
|
64
|
-
if (isFullscreen) {
|
|
65
|
-
return (
|
|
66
|
-
<div className="fullscreen-view">
|
|
67
|
-
{/* Expanded layout */}
|
|
68
|
-
<button onClick={() => setDisplayMode("inline")}>Collapse</button>
|
|
69
|
-
</div>
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return (
|
|
74
|
-
<div className="inline-view">
|
|
75
|
-
{/* Compact layout */}
|
|
76
|
-
<button onClick={() => setDisplayMode("fullscreen")}>Expand</button>
|
|
77
|
-
</div>
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
## Modal
|
|
83
|
-
|
|
84
|
-
Overlay rendered outside the view iframe, on top of the current display mode.
|
|
85
|
-
|
|
86
|
-
**Use for:** Confirmations, additional input before an action.
|
|
87
|
-
|
|
88
|
-
**Constraints:**
|
|
89
|
-
- Triggered by user interaction only
|
|
90
|
-
- Host injects close controls
|
|
91
|
-
|
|
92
|
-
```tsx
|
|
93
|
-
import { useRequestModal } from "skybridge/web";
|
|
94
|
-
|
|
95
|
-
function SettingsView() {
|
|
96
|
-
const { isOpen, open, params } = useRequestModal();
|
|
97
|
-
|
|
98
|
-
if (isOpen) {
|
|
99
|
-
return (
|
|
100
|
-
<div className="modal">
|
|
101
|
-
<h2>Are you sure?</h2>
|
|
102
|
-
<p>This will delete item {params.itemId}</p>
|
|
103
|
-
<button onClick={() => console.log("Confirmed")}>Yes, Delete</button>
|
|
104
|
-
<button onClick={() => console.log("Cancelled")}>Cancel</button>
|
|
105
|
-
</div>
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return (
|
|
110
|
-
<button onClick={() => open({ title: "Confirm", params: { itemId: "123" } })}>
|
|
111
|
-
Delete Item
|
|
112
|
-
</button>
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
## Adapting to Host
|
|
118
|
-
|
|
119
|
-
Use `useLayout` to read host environment constraints.
|
|
120
|
-
|
|
121
|
-
### Layout Constraints
|
|
122
|
-
|
|
123
|
-
- `maxHeight`: Maximum height available for the view in pixels
|
|
124
|
-
- `safeArea.insets`: Padding to avoid device notches, composer overlay, and navigation bars
|
|
125
|
-
|
|
126
|
-
```tsx
|
|
127
|
-
import { useLayout } from "skybridge/web";
|
|
128
|
-
|
|
129
|
-
function Container({ children }) {
|
|
130
|
-
const { maxHeight, safeArea } = useLayout();
|
|
131
|
-
const { top, right, bottom, left } = safeArea.insets;
|
|
132
|
-
|
|
133
|
-
return (
|
|
134
|
-
<div style={{ maxHeight, padding: `${top}px ${right}px ${bottom}px ${left}px` }}>
|
|
135
|
-
{children}
|
|
136
|
-
</div>
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
### Theme
|
|
142
|
-
|
|
143
|
-
Match the host color scheme using `theme` from `useLayout`.
|
|
144
|
-
|
|
145
|
-
```tsx
|
|
146
|
-
import { useLayout } from "skybridge/web";
|
|
147
|
-
|
|
148
|
-
function Container({ children }) {
|
|
149
|
-
const { theme } = useLayout();
|
|
150
|
-
const isDark = theme === "dark";
|
|
151
|
-
|
|
152
|
-
return <div className={isDark ? "bg-surface-dark" : "bg-surface-light"}>{children}</div>;
|
|
153
|
-
}
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
## Adapting to User
|
|
157
|
-
|
|
158
|
-
Use `useUser` to read user context.
|
|
159
|
-
|
|
160
|
-
### Device
|
|
161
|
-
|
|
162
|
-
- `device.type`: `"mobile" | "tablet" | "desktop" | "unknown"`
|
|
163
|
-
- `capabilities.hover`: `true` if device supports hover (mouse)
|
|
164
|
-
- `capabilities.touch`: `true` if device supports touch
|
|
165
|
-
|
|
166
|
-
```tsx
|
|
167
|
-
import { useUser } from "skybridge/web";
|
|
168
|
-
|
|
169
|
-
function ProductCard({ product }) {
|
|
170
|
-
const { userAgent } = useUser();
|
|
171
|
-
const { device, capabilities } = userAgent;
|
|
172
|
-
|
|
173
|
-
return (
|
|
174
|
-
<div className={capabilities.hover ? "hover:shadow-lg" : ""}>
|
|
175
|
-
<img src={product.image} alt={product.name} />
|
|
176
|
-
<h3>{product.name}</h3>
|
|
177
|
-
{device.type === "mobile" && <button>Add to Cart</button>}
|
|
178
|
-
{capabilities.touch && <p className="hint">Swipe for more</p>}
|
|
179
|
-
</div>
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
### Locale
|
|
185
|
-
|
|
186
|
-
Use `locale` from `useUser` to adapt content to user's language.
|
|
187
|
-
|
|
188
|
-
```tsx
|
|
189
|
-
import { useUser } from "skybridge/web";
|
|
190
|
-
|
|
191
|
-
function LocalizedGreeting() {
|
|
192
|
-
const { locale } = useUser();
|
|
193
|
-
|
|
194
|
-
const greetings = {
|
|
195
|
-
en: "Hello!",
|
|
196
|
-
fr: "Bonjour!",
|
|
197
|
-
zh: "你好!",
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
const language = locale.split("-")[0];
|
|
201
|
-
const greeting = greetings[language] || greetings.en;
|
|
202
|
-
|
|
203
|
-
return <h1>{greeting}</h1>;
|
|
204
|
-
}
|
|
205
|
-
```
|