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.
Files changed (97) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cli.js +314 -86
  3. package/package.json +1 -1
  4. package/template/.agents/skills/agent-browser/SKILL.md +4 -9
  5. package/template/.agents/skills/apps/skybridge/SKILL.md +1 -1
  6. package/template/.agents/skills/code/improve/SKILL.md +9 -5
  7. package/template/.agents/skills/code/improve/references/audit-playbook.md +10 -10
  8. package/template/.agents/skills/code/improve/references/closing-the-loop.md +4 -3
  9. package/template/.agents/skills/code/improve/references/plan-template.md +5 -0
  10. package/template/.agents/skills/design/impeccable/SKILL.md +14 -24
  11. package/template/.agents/skills/design/impeccable/reference/animate.md +1 -1
  12. package/template/.agents/skills/design/impeccable/reference/bolder.md +1 -1
  13. package/template/.agents/skills/design/impeccable/reference/brand.md +2 -2
  14. package/template/.agents/skills/design/impeccable/reference/colorize.md +1 -1
  15. package/template/.agents/skills/design/impeccable/reference/critique.md +6 -6
  16. package/template/.agents/skills/design/impeccable/reference/delight.md +1 -1
  17. package/template/.agents/skills/design/impeccable/reference/distill.md +1 -1
  18. package/template/.agents/skills/design/impeccable/reference/document.md +1 -1
  19. package/template/.agents/skills/design/impeccable/reference/extract.md +1 -1
  20. package/template/.agents/skills/design/impeccable/reference/hooks.md +90 -0
  21. package/template/.agents/skills/design/impeccable/reference/init.md +5 -5
  22. package/template/.agents/skills/design/impeccable/reference/live.md +16 -16
  23. package/template/.agents/skills/design/impeccable/reference/overdrive.md +1 -1
  24. package/template/.agents/skills/design/impeccable/reference/polish.md +2 -2
  25. package/template/.agents/skills/design/impeccable/reference/quieter.md +1 -1
  26. package/template/.agents/skills/design/impeccable/reference/shape.md +2 -2
  27. package/template/.agents/skills/design/impeccable/scripts/context-signals.mjs +1 -1
  28. package/template/.agents/skills/design/impeccable/scripts/context.mjs +724 -33
  29. package/template/.agents/skills/design/impeccable/scripts/critique-storage.mjs +1 -1
  30. package/template/.agents/skills/design/impeccable/scripts/detector/browser/injected/index.mjs +204 -0
  31. package/template/.agents/skills/design/impeccable/scripts/detector/cli/main.mjs +57 -11
  32. package/template/.agents/skills/design/impeccable/scripts/detector/design-system.mjs +750 -0
  33. package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns-browser.js +633 -46
  34. package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns.mjs +7 -0
  35. package/template/.agents/skills/design/impeccable/scripts/detector/engines/browser/detect-url.mjs +29 -4
  36. package/template/.agents/skills/design/impeccable/scripts/detector/engines/regex/detect-text.mjs +43 -10
  37. package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +29 -0
  38. package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/detect-html.mjs +27 -1
  39. package/template/.agents/skills/design/impeccable/scripts/detector/node/file-system.mjs +1 -1
  40. package/template/.agents/skills/design/impeccable/scripts/detector/registry/antipatterns.mjs +29 -0
  41. package/template/.agents/skills/design/impeccable/scripts/detector/rules/checks.mjs +401 -46
  42. package/template/.agents/skills/design/impeccable/scripts/detector/shared/inline-ignores.mjs +148 -0
  43. package/template/.agents/skills/design/impeccable/scripts/hook-admin.mjs +661 -0
  44. package/template/.agents/skills/design/impeccable/scripts/hook-before-edit.mjs +476 -0
  45. package/template/.agents/skills/design/impeccable/scripts/hook-lib.mjs +1632 -0
  46. package/template/.agents/skills/design/impeccable/scripts/hook.mjs +61 -0
  47. package/template/.agents/skills/design/impeccable/scripts/{design-parser.mjs → lib/design-parser.mjs} +8 -1
  48. package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-config.mjs +638 -0
  49. package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-paths.mjs +128 -0
  50. package/template/.agents/skills/design/impeccable/scripts/lib/target-args.mjs +42 -0
  51. package/template/.agents/skills/design/impeccable/scripts/live/browser-script-parts.mjs +49 -0
  52. package/template/.agents/skills/design/impeccable/scripts/{live-event-validation.mjs → live/event-validation.mjs} +6 -5
  53. package/template/.agents/skills/design/impeccable/scripts/live/manual-apply.mjs +939 -0
  54. package/template/.agents/skills/design/impeccable/scripts/live/manual-edit-routes.mjs +357 -0
  55. package/template/.agents/skills/design/impeccable/scripts/{live-manual-edits-buffer.mjs → live/manual-edits-buffer.mjs} +1 -1
  56. package/template/.agents/skills/design/impeccable/scripts/{live-session-store.mjs → live/session-store.mjs} +1 -1
  57. package/template/.agents/skills/design/impeccable/scripts/{live-ui-core.mjs → live/ui-core.mjs} +2 -1
  58. package/template/.agents/skills/design/impeccable/scripts/live/vocabulary.mjs +36 -0
  59. package/template/.agents/skills/design/impeccable/scripts/live-accept.mjs +3 -3
  60. package/template/.agents/skills/design/impeccable/scripts/live-browser-dom.js +146 -0
  61. package/template/.agents/skills/design/impeccable/scripts/live-browser.js +1456 -599
  62. package/template/.agents/skills/design/impeccable/scripts/live-commit-manual-edits.mjs +2 -2
  63. package/template/.agents/skills/design/impeccable/scripts/live-complete.mjs +2 -2
  64. package/template/.agents/skills/design/impeccable/scripts/live-discard-manual-edits.mjs +1 -1
  65. package/template/.agents/skills/design/impeccable/scripts/live-inject.mjs +35 -9
  66. package/template/.agents/skills/design/impeccable/scripts/live-insert.mjs +2 -2
  67. package/template/.agents/skills/design/impeccable/scripts/live-manual-edit-evidence.mjs +2 -2
  68. package/template/.agents/skills/design/impeccable/scripts/live-poll.mjs +18 -13
  69. package/template/.agents/skills/design/impeccable/scripts/live-resume.mjs +1 -1
  70. package/template/.agents/skills/design/impeccable/scripts/live-server.mjs +77 -1264
  71. package/template/.agents/skills/design/impeccable/scripts/live-status.mjs +2 -2
  72. package/template/.agents/skills/design/impeccable/scripts/live-target.mjs +30 -0
  73. package/template/.agents/skills/design/impeccable/scripts/live-wrap.mjs +4 -4
  74. package/template/.agents/skills/design/impeccable/scripts/live.mjs +73 -22
  75. package/template/.agents/skills/writing/humanizer/SKILL.md +621 -0
  76. package/template/.agents/upstreams.json +17 -8
  77. package/template/.agents/skills/apps/skybridge/references/architecture.md +0 -175
  78. package/template/.agents/skills/apps/skybridge/references/copy-template.md +0 -24
  79. package/template/.agents/skills/apps/skybridge/references/csp.md +0 -33
  80. package/template/.agents/skills/apps/skybridge/references/deploy.md +0 -33
  81. package/template/.agents/skills/apps/skybridge/references/discover.md +0 -84
  82. package/template/.agents/skills/apps/skybridge/references/download-file.md +0 -77
  83. package/template/.agents/skills/apps/skybridge/references/fetch-and-render-data.md +0 -151
  84. package/template/.agents/skills/apps/skybridge/references/oauth.md +0 -115
  85. package/template/.agents/skills/apps/skybridge/references/open-external-links.md +0 -71
  86. package/template/.agents/skills/apps/skybridge/references/prompt-llm.md +0 -20
  87. package/template/.agents/skills/apps/skybridge/references/publish.md +0 -19
  88. package/template/.agents/skills/apps/skybridge/references/run-locally.md +0 -51
  89. package/template/.agents/skills/apps/skybridge/references/state-and-context.md +0 -151
  90. package/template/.agents/skills/apps/skybridge/references/ui-guidelines.md +0 -205
  91. package/template/.agents/skills/design/impeccable/scripts/cleanup-deprecated.mjs +0 -284
  92. package/template/.agents/skills/design/impeccable/scripts/impeccable-paths.mjs +0 -126
  93. /package/template/.agents/skills/design/impeccable/scripts/{is-generated.mjs → lib/is-generated.mjs} +0 -0
  94. /package/template/.agents/skills/design/impeccable/scripts/{live-completion.mjs → live/completion.mjs} +0 -0
  95. /package/template/.agents/skills/design/impeccable/scripts/{live-insert-ui.mjs → live/insert-ui.mjs} +0 -0
  96. /package/template/.agents/skills/design/impeccable/scripts/{live-svelte-component.mjs → live/svelte-component.mjs} +0 -0
  97. /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
- ```