Need UI components? For buttons, inputs, layouts, and charts, see the Extension Kit Components guide.
All imports come from @teachfloor/extension-kit, which is automatically installed when you create an app.
Signaling App Ready
Basic Usage
Signal to the platform that your app is ready:
Code
import { initialize } from '@teachfloor/extension-kit'function App() { useEffect(() => { initialize() // Signal app is ready }, []) return <div>My App</div>}
With Context Provider
Code
import { ExtensionContextProvider } from '@teachfloor/extension-kit'// The provider signals readiness automatically<ExtensionContextProvider autoInit={true}> <App /></ExtensionContextProvider>
API Reference
Events
Subscribe to Events
Listen to platform events using subscribeToEvent(). Events receive two parameters: the event data and an objectContext containing contextual information based on your app's permissions.
Code
import { subscribeToEvent } from '@teachfloor/extension-kit'// Viewport changes - objectContext includes course/module/element based on permissionssubscribeToEvent('environment.viewport.changed', (viewport, objectContext) => { console.log('User navigated to:', viewport) if (objectContext.course) { console.log('Course:', objectContext.course.name) } if (objectContext.module) { console.log('Module:', objectContext.module.name) } if (objectContext.element) { console.log('Element:', objectContext.element.name) }})// Path changes - single parametersubscribeToEvent('environment.path.changed', (path) => { console.log('Path changed:', path)})// User events - objectContext includes contextual datasubscribeToEvent('auth.user.event', (eventData, objectContext) => { console.log('User event:', eventData.type) if (objectContext.course) { console.log('Happened in course:', objectContext.course.name) }})
Available Events:
Event
Parameters
Description
environment.viewport.changed
(viewport, objectContext)
User navigated to a different viewport. objectContext contains course/module/element data based on permissions.
Objects are only present when the permission is granted and the viewport context matches.
How it works:
Objects are included based on two conditions:
Permission granted: Your app must have the corresponding permission declared in the manifest
Relevant context: The current viewport/path must be within that context
When both conditions are met, the complete object is included. Otherwise, the key is not present in the object.
Example:
Code
// With courses:read permission on a course detail page:{ course: { id: "abc123", name: "Introduction to React", /* ...other course fields */ } // module and element keys not present (not in their context)}// Without courses:read permission on a course detail page:{ // No keys present (no permissions granted)}// With courses:read and modules:read on a module detail page:{ course: { id: "abc123", name: "Introduction to React", /* ...other course fields */ }, module: { id: "def456", name: "Getting Started", /* ...other module fields */ } // element key not present (not in element context)}
See Permissions for details on requesting access to course, module, and element data.
Data Storage
The Extension Kit provides store(), retrieve(), createStorage(), and createCollection() functions for persisting data. Three types of storage are available: app data (organization-wide), user data (user-specific), and user collections (paginated lists). createStorage() is the recommended wrapper over store / retrieve, adding namespaced keys, TTL, and query() for paged filter/sort iteration.
See Data Storage for complete documentation, API reference, and usage examples.
interface Context { userContext: { id: string created_at: string full_name: string email: string avatar: string language: string timezone: string identity_provider: { // SSO/Identity provider details (null if not configured) provider: string // Provider name (e.g., 'auth0', 'saml', 'neoncrm') user_id: string | null // User ID in the identity provider user_metadata: object // Additional user metadata from provider } | null } appContext: { id: string name: string version: string // Current installed version permissions: string[] // Array of granted permission scopes views: array // Array of app view configurations from manifest } environment: { initialized: boolean viewport: string path: string surface: 'drawer' | 'page' | 'widget' presentation: 'default' | 'modal' // 'modal' when the widget was opened via openModal() } // App-provided payload passed to openModal({ state }). Set once at // mount and does not update. // null when no launch state was provided. state: object | null}
Reactive Context
Context updates automatically when user data changes:
Code
function UserGreeting() { const { userContext } = useExtensionContext() // Re-renders when userContext changes return <h1>Hello, {userContext.full_name}!</h1>}
UI Integration
Toast Notifications
Code
import { showToast } from '@teachfloor/extension-kit'// Success messageshowToast('Changes saved successfully', { color: 'green' })// Error messageshowToast('Failed to save changes', { color: 'red' })// Info messageshowToast('Processing your request', { color: 'blue' })// Warning messageshowToast('Please review your input', { color: 'orange' })
Widget-surface views can promote themselves into a modal for more room — useful for detail views, forms, or wizards triggered from a compact dashboard widget. The widget always opens ITSELF in the modal (never a different widget), and the modal runs the same component with a fresh mount.
Code
import { openModal, closeModal, useExtensionContext } from '@teachfloor/extension-kit'function MyWidget() { const { environment } = useExtensionContext() const isModal = environment.presentation === 'modal' // Compact rendering in the widget slot, full rendering in the modal if (!isModal) { return <button onClick={() => openModal({ size: 'lg' })}>Expand</button> } return ( <div> <FullDetailView /> <button onClick={() => closeModal()}>Done</button> </div> )}
openModal(options)
Available on the widget surface only. Calls from drawer or page surfaces are silently ignored.
Anything outside the allowed values is dropped by the host — you can't pass arbitrary strings through to the underlying modal.
closeModal()
Dismiss the modal from within its own view. Only meaningful when environment.presentation === 'modal'. Typical use: form submit success, wizard finish. Calls from a normally-placed widget/drawer/page are no-ops.
Launch state — openModal({ state }) + useLaunchState()
Passing state in openModal hands an arbitrary object to the modal-hosted view. The child reads it via useLaunchState() (or useExtensionContext().state). Set once at mount, does not update. Use it for deep-linking, initial form values, or opener context.
Code
import { openModal, useLaunchState } from '@teachfloor/extension-kit'// In the widget slot:function CompactWidget() { return ( <button onClick={() => openModal({ size: 'lg', state: { noteId: 'abc-123', mode: 'edit' }, })}> Edit note </button> )}// In the same widget rendered in the modal:function ModalView() { const launchState = useLaunchState() // { noteId: 'abc-123', mode: 'edit' } — or null if (!launchState?.noteId) return <NewNoteForm /> return <EditNoteForm noteId={launchState.noteId} mode={launchState.mode} />}
useLaunchState() returns null when the view wasn't launched with state (i.e. rendered directly in its widget slot, not opened via openModal).
Navigation
Code
import { goToViewport } from '@teachfloor/extension-kit'// Navigate to coursesfunction goToCourses() { goToViewport('teachfloor.dashboard.course.list')}// Navigate to settingsfunction goToSettings() { goToViewport('teachfloor.dashboard.settings.general.detail')}// Navigate to accountfunction goToAccount() { goToViewport('teachfloor.dashboard.account.detail')}
Deeplinking
Use goToPath when you already have a fully-resolved in-app path — typically captured from environment.path — and want to deeplink the user back to it (e.g. a saved bookmark or a "back to where you were" button).
Code
import { goToPath, useExtensionContext } from '@teachfloor/extension-kit'// Save the current path...const { environment } = useExtensionContext()const savedPath = environment.path // e.g. "/org-slug/courses/123/modules/456"// ...and deeplink back latergoToPath(savedPath)
Rules and guards:
path must be a relative path beginning with a single / (no protocol-relative //…, no absolute URLs). Anything else is ignored.
The path must belong to the current organization — deeplinks whose first segment doesn't match the user's org slug are rejected, so an app installed in one org can't redirect the user into another.
On custom domains, the org slug is stripped from the URL automatically — paths captured from environment.path work on both URL shapes.
AI Generation
Text Generation
Generate text using AI models with the platform's built-in AI capabilities.
Code
import { generate } from '@teachfloor/extension-kit'// Generate textasync function generateContent() { try { const result = await generate( 'Write a summary of this course', 'ai/text-generate' ) console.log(result) // Generated text response return result } catch (error) { console.error('Generation failed:', error) }}
Parameters:
prompt (string, required): The prompt to send to the AI model. Can include placeholders like {{course.content}}
generationType (string, optional): Type of generation (default: 'ai/text-generate')
Available Generation Types:
'ai/text-generate': General text generation
Permissions Required:
ai:text_generate: Always required to use AI generation
ai:context_external_send: Only required when using placeholders
Contextual permissions: Required for corresponding placeholders (courses:read for course placeholders, etc.)
Using Placeholders
Include platform data directly in prompts using placeholders:
Code
import { generate } from '@teachfloor/extension-kit'// Course placeholders (requires: ai:text_generate + courses:read + ai:context_external_send)const courseSummary = await generate( 'Summarize this course: {{course.content}}')// Module placeholders (requires: ai:text_generate + modules:read + ai:context_external_send)const moduleQuiz = await generate( 'Create a 5-question quiz about {{module.name}}: {{module.content}}')// Element placeholders (requires: ai:text_generate + elements:read + ai:context_external_send)const studyNotes = await generate( 'Generate study notes for {{element.name}}: {{element.content}}')
Supported Placeholders:
{{course.name}} - Course title
{{course.content}} - Course content (text format)
{{module.name}} - Module title
{{module.content}} - Module content (text format)
{{element.name}} - Element title
{{element.content}} - Element content (text format)
Without Placeholders
You can also manually include context data (only requires ai:text_generate):