Separate viewport and containerDimensions
This commit is contained in:
parent
c22e86233b
commit
82e64d56bb
6 changed files with 320 additions and 110 deletions
|
|
@ -461,16 +461,20 @@ interface HostContext {
|
|||
displayMode?: "inline" | "fullscreen" | "pip";
|
||||
/** Display modes the host supports */
|
||||
availableDisplayModes?: string[];
|
||||
/** Current and maximum dimensions available to the UI. */
|
||||
/** Container dimensions for the iframe. Specify either width or maxWidth, and either height or maxHeight. */
|
||||
containerDimensions?: (
|
||||
| { height: number } // If specified, container is fixed at this height
|
||||
| { maxHeight?: number } // Otherwise, container height is determined by the UI height, up to this maximum height (if defined)
|
||||
) & (
|
||||
| { width: number } // If specified, container is fixed at this width
|
||||
| { maxWidth?: number } // Otherwise, container width is determined by the UI width, up to this maximum width (if defined)
|
||||
);
|
||||
/** Host window viewport dimensions */
|
||||
viewport?: {
|
||||
/** Viewport width (if fixed). Only pass width or maxWidth, not both. */
|
||||
/** Window viewport width in pixels. */
|
||||
width?: number;
|
||||
/** Viewport height (if fixed). Only pass height or maxHeight, not both. */
|
||||
/** Window viewport height in pixels. */
|
||||
height?: number;
|
||||
/** Maximum available viewport width in pixels (if constrained). */
|
||||
maxWidth?: number;
|
||||
/** Maximum available viewport height in pixels (if constrained). */
|
||||
maxHeight?: number;
|
||||
};
|
||||
/** User's language/region preference (BCP 47, e.g., "en-US") */
|
||||
locale?: string;
|
||||
|
|
@ -520,20 +524,25 @@ Example:
|
|||
}
|
||||
},
|
||||
"displayMode": "inline",
|
||||
"viewport": { "width": 400, "maxHeight": 600 }
|
||||
"containerDimensions": { "width": 400, "maxHeight": 600 }
|
||||
"viewport": { "width": 1920, "height": 1080 },
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Viewport and Sizing
|
||||
### Viewport and Dimensions
|
||||
|
||||
The `viewport` field in `HostContext` communicates sizing constraints between host and app. Each dimension (height and width) operates independently and can be either **fixed** or **flexible**.
|
||||
The `HostContext` provides two separate fields for sizing information:
|
||||
|
||||
#### Viewport Modes
|
||||
- **`containerDimensions`**: The dimensions of the container that holds the app. This controls the actual space the app occupies within the host. Each dimension (height and width) operates independently and can be either **fixed** or **flexible**.
|
||||
|
||||
| Mode | Viewport Field | Meaning |
|
||||
|------|---------------|---------|
|
||||
- **`viewport`**: The host window's dimensions (e.g., `window.innerWidth` and `window.innerHeight`). Apps can use this to make responsive layout decisions based on the overall screen size.
|
||||
|
||||
#### Dimension Modes
|
||||
|
||||
| Mode | Dimensions Field | Meaning |
|
||||
|------|-----------------|---------|
|
||||
| Fixed | `height` or `width` | Host controls the size. App should fill the available space. |
|
||||
| Flexible | `maxHeight` or `maxWidth` | App controls the size, up to the specified maximum. |
|
||||
| Unbounded | Field omitted | App controls the size with no limit. |
|
||||
|
|
@ -542,33 +551,39 @@ These modes can be combined independently. For example, a host might specify a f
|
|||
|
||||
#### App Behavior
|
||||
|
||||
Apps should check the viewport configuration and apply appropriate CSS:
|
||||
Apps should check the containerDimensions configuration and apply appropriate CSS:
|
||||
|
||||
```typescript
|
||||
// In the app's initialization
|
||||
const viewport = hostContext.viewport;
|
||||
const containerDimensions = hostContext.containerDimensions;
|
||||
|
||||
if (viewport) {
|
||||
if (containerDimensions) {
|
||||
// Handle height
|
||||
if ("height" in viewport) {
|
||||
if ("height" in containerDimensions) {
|
||||
// Fixed height: fill the container
|
||||
document.documentElement.style.height = "100vh";
|
||||
} else if (viewport.maxHeight) {
|
||||
} else if ("maxHeight" in containerDimensions && containerDimensions.maxHeight) {
|
||||
// Flexible with max: let content determine size, up to max
|
||||
document.documentElement.style.maxHeight = `${viewport.maxHeight}px`;
|
||||
document.documentElement.style.maxHeight = `${containerDimensions.maxHeight}px`;
|
||||
}
|
||||
// If neither, height is unbounded
|
||||
|
||||
// Handle width
|
||||
if ("width" in viewport) {
|
||||
if ("width" in containerDimensions) {
|
||||
// Fixed width: fill the container
|
||||
document.documentElement.style.width = "100vw";
|
||||
} else if (viewport.maxWidth) {
|
||||
} else if ("maxWidth" in containerDimensions && containerDimensions.maxWidth) {
|
||||
// Flexible with max: let content determine size, up to max
|
||||
document.documentElement.style.maxWidth = `${viewport.maxWidth}px`;
|
||||
document.documentElement.style.maxWidth = `${containerDimensions.maxWidth}px`;
|
||||
}
|
||||
// If neither, width is unbounded
|
||||
}
|
||||
|
||||
// Apps can also use viewport for additional data to make responsive layout decisions
|
||||
const viewport = hostContext.viewport;
|
||||
if (viewport?.width && viewport.width < 768) {
|
||||
// Apply mobile-friendly layout
|
||||
}
|
||||
```
|
||||
|
||||
#### Host Behavior
|
||||
|
|
|
|||
|
|
@ -113,7 +113,8 @@ describe("App <-> AppBridge integration", () => {
|
|||
const testHostContext = {
|
||||
theme: "dark" as const,
|
||||
locale: "en-US",
|
||||
viewport: { width: 800, height: 600, maxHeight: 600 },
|
||||
viewport: { width: 800, height: 600 },
|
||||
containerDimensions: { width: 800, maxHeight: 600 },
|
||||
};
|
||||
const newBridge = new AppBridge(
|
||||
createMockClient() as Client,
|
||||
|
|
@ -337,7 +338,8 @@ describe("App <-> AppBridge integration", () => {
|
|||
const initialContext = {
|
||||
theme: "light" as const,
|
||||
locale: "en-US",
|
||||
viewport: { width: 800, height: 600, maxHeight: 600 },
|
||||
viewport: { width: 800, height: 600 },
|
||||
containerDimensions: { width: 800, maxHeight: 600 },
|
||||
};
|
||||
const newBridge = new AppBridge(
|
||||
createMockClient() as Client,
|
||||
|
|
@ -354,22 +356,26 @@ describe("App <-> AppBridge integration", () => {
|
|||
newBridge.sendHostContextChange({ theme: "dark" });
|
||||
await flush();
|
||||
|
||||
// Send another partial update: only viewport changes
|
||||
// Send another partial update: only viewport and containerDimensions change
|
||||
newBridge.sendHostContextChange({
|
||||
viewport: { width: 1024, height: 768, maxHeight: 768 },
|
||||
viewport: { width: 1024, height: 768 },
|
||||
containerDimensions: { width: 1024, maxHeight: 768 },
|
||||
});
|
||||
await flush();
|
||||
|
||||
// getHostContext should have accumulated all updates:
|
||||
// - locale from initial (unchanged)
|
||||
// - theme from first partial update
|
||||
// - viewport from second partial update
|
||||
// - viewport and containerDimensions from second partial update
|
||||
const context = newApp.getHostContext();
|
||||
expect(context?.theme).toBe("dark");
|
||||
expect(context?.locale).toBe("en-US");
|
||||
expect(context?.viewport).toEqual({
|
||||
width: 1024,
|
||||
height: 768,
|
||||
});
|
||||
expect(context?.containerDimensions).toEqual({
|
||||
width: 1024,
|
||||
maxHeight: 768,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1008,7 +1008,8 @@ export class AppBridge extends Protocol<
|
|||
* ```typescript
|
||||
* bridge.setHostContext({
|
||||
* theme: "dark",
|
||||
* viewport: { width: 800, maxHeight: 600 }
|
||||
* viewport: { width: 800, height: 600 },
|
||||
* containerDimensions: { maxHeight: 600, width: 800 }
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
|
|
|
|||
267
src/generated/schema.json
generated
267
src/generated/schema.json
generated
|
|
@ -632,12 +632,77 @@
|
|||
"type": "string"
|
||||
}
|
||||
},
|
||||
"containerDimensions": {
|
||||
"description": "Container dimensions. Represents the dimensions of the iframe or other\ncontainer holding the app. Specify either width or maxWidth, and either height or maxHeight.",
|
||||
"allOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"height": {
|
||||
"description": "Fixed container height in pixels.",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["height"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"maxHeight": {
|
||||
"description": "Maximum container height in pixels.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"width": {
|
||||
"description": "Fixed container width in pixels.",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["width"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"maxWidth": {
|
||||
"description": "Maximum container width in pixels.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"viewport": {
|
||||
"description": "Current and maximum dimensions available to the UI.",
|
||||
"description": "Window viewport dimensions. Represents the host window's viewport size,\nwhich provides additional information apps can use to make responsive layout decisions.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"width": {
|
||||
"description": "Viewport width in pixels (if fixed). Only pass width or maxWidth, not both.",
|
||||
"description": "Window viewport width in pixels.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
|
|
@ -646,25 +711,7 @@
|
|||
]
|
||||
},
|
||||
"height": {
|
||||
"description": "Viewport height in pixels (if fixed). Only pass height or maxHeight, not both.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{}
|
||||
]
|
||||
},
|
||||
"maxWidth": {
|
||||
"description": "Maximum available viewport width in pixels (if constrained). Only pass width or maxWidth, not both.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{}
|
||||
]
|
||||
},
|
||||
"maxHeight": {
|
||||
"description": "Maximum available viewport height in pixels (if constrained). Only pass height or maxHeight, not both.",
|
||||
"description": "Window viewport height in pixels.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
|
|
@ -1281,12 +1328,77 @@
|
|||
"type": "string"
|
||||
}
|
||||
},
|
||||
"containerDimensions": {
|
||||
"description": "Container dimensions. Represents the dimensions of the iframe or other\ncontainer holding the app. Specify either width or maxWidth, and either height or maxHeight.",
|
||||
"allOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"height": {
|
||||
"description": "Fixed container height in pixels.",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["height"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"maxHeight": {
|
||||
"description": "Maximum container height in pixels.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"width": {
|
||||
"description": "Fixed container width in pixels.",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["width"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"maxWidth": {
|
||||
"description": "Maximum container width in pixels.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"viewport": {
|
||||
"description": "Current and maximum dimensions available to the UI.",
|
||||
"description": "Window viewport dimensions. Represents the host window's viewport size,\nwhich provides additional information apps can use to make responsive layout decisions.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"width": {
|
||||
"description": "Viewport width in pixels (if fixed). Only pass width or maxWidth, not both.",
|
||||
"description": "Window viewport width in pixels.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
|
|
@ -1295,25 +1407,7 @@
|
|||
]
|
||||
},
|
||||
"height": {
|
||||
"description": "Viewport height in pixels (if fixed). Only pass height or maxHeight, not both.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{}
|
||||
]
|
||||
},
|
||||
"maxWidth": {
|
||||
"description": "Maximum available viewport width in pixels (if constrained). Only pass width or maxWidth, not both.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{}
|
||||
]
|
||||
},
|
||||
"maxHeight": {
|
||||
"description": "Maximum available viewport height in pixels (if constrained). Only pass height or maxHeight, not both.",
|
||||
"description": "Window viewport height in pixels.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
|
|
@ -2467,12 +2561,77 @@
|
|||
"type": "string"
|
||||
}
|
||||
},
|
||||
"containerDimensions": {
|
||||
"description": "Container dimensions. Represents the dimensions of the iframe or other\ncontainer holding the app. Specify either width or maxWidth, and either height or maxHeight.",
|
||||
"allOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"height": {
|
||||
"description": "Fixed container height in pixels.",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["height"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"maxHeight": {
|
||||
"description": "Maximum container height in pixels.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"width": {
|
||||
"description": "Fixed container width in pixels.",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["width"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"maxWidth": {
|
||||
"description": "Maximum container width in pixels.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"viewport": {
|
||||
"description": "Current and maximum dimensions available to the UI.",
|
||||
"description": "Window viewport dimensions. Represents the host window's viewport size,\nwhich provides additional information apps can use to make responsive layout decisions.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"width": {
|
||||
"description": "Viewport width in pixels (if fixed). Only pass width or maxWidth, not both.",
|
||||
"description": "Window viewport width in pixels.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
|
|
@ -2481,25 +2640,7 @@
|
|||
]
|
||||
},
|
||||
"height": {
|
||||
"description": "Viewport height in pixels (if fixed). Only pass height or maxHeight, not both.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{}
|
||||
]
|
||||
},
|
||||
"maxWidth": {
|
||||
"description": "Maximum available viewport width in pixels (if constrained). Only pass width or maxWidth, not both.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{}
|
||||
]
|
||||
},
|
||||
"maxHeight": {
|
||||
"description": "Maximum available viewport height in pixels (if constrained). Only pass height or maxHeight, not both.",
|
||||
"description": "Window viewport height in pixels.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
|
|
|
|||
72
src/generated/schema.ts
generated
72
src/generated/schema.ts
generated
|
|
@ -563,40 +563,64 @@ export const McpUiHostContextSchema = z.looseObject({
|
|||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Display modes the host supports."),
|
||||
/** @description Current and maximum dimensions available to the UI. */
|
||||
/**
|
||||
* @description Container dimensions. Represents the dimensions of the iframe or other
|
||||
* container holding the app. Specify either width or maxWidth, and either height or maxHeight.
|
||||
*/
|
||||
containerDimensions: z
|
||||
.union([
|
||||
z.object({
|
||||
/** @description Fixed container height in pixels. */
|
||||
height: z.number().describe("Fixed container height in pixels."),
|
||||
}),
|
||||
z.object({
|
||||
/** @description Maximum container height in pixels. */
|
||||
maxHeight: z
|
||||
.union([z.number(), z.undefined()])
|
||||
.optional()
|
||||
.describe("Maximum container height in pixels."),
|
||||
}),
|
||||
])
|
||||
.and(
|
||||
z.union([
|
||||
z.object({
|
||||
/** @description Fixed container width in pixels. */
|
||||
width: z.number().describe("Fixed container width in pixels."),
|
||||
}),
|
||||
z.object({
|
||||
/** @description Maximum container width in pixels. */
|
||||
maxWidth: z
|
||||
.union([z.number(), z.undefined()])
|
||||
.optional()
|
||||
.describe("Maximum container width in pixels."),
|
||||
}),
|
||||
]),
|
||||
)
|
||||
.optional()
|
||||
.describe(
|
||||
"Container dimensions. Represents the dimensions of the iframe or other\ncontainer holding the app. Specify either width or maxWidth, and either height or maxHeight.",
|
||||
),
|
||||
/**
|
||||
* @description Window viewport dimensions. Represents the host window's viewport size,
|
||||
* which provides additional information apps can use to make responsive layout decisions.
|
||||
*/
|
||||
viewport: z
|
||||
.object({
|
||||
/** @description Viewport width in pixels (if fixed). Only pass width or maxWidth, not both. */
|
||||
/** @description Window viewport width in pixels. */
|
||||
width: z
|
||||
.union([z.number(), z.undefined()])
|
||||
.optional()
|
||||
.describe(
|
||||
"Viewport width in pixels (if fixed). Only pass width or maxWidth, not both.",
|
||||
),
|
||||
/** @description Viewport height in pixels (if fixed). Only pass height or maxHeight, not both. */
|
||||
.describe("Window viewport width in pixels."),
|
||||
/** @description Window viewport height in pixels. */
|
||||
height: z
|
||||
.union([z.number(), z.undefined()])
|
||||
.optional()
|
||||
.describe(
|
||||
"Viewport height in pixels (if fixed). Only pass height or maxHeight, not both.",
|
||||
),
|
||||
/** @description Maximum available viewport width in pixels (if constrained). Only pass width or maxWidth, not both.*/
|
||||
maxWidth: z
|
||||
.union([z.number(), z.undefined()])
|
||||
.optional()
|
||||
.describe(
|
||||
"Maximum available viewport width in pixels (if constrained). Only pass width or maxWidth, not both.",
|
||||
),
|
||||
/** @description Maximum available viewport height in pixels (if constrained). Only pass height or maxHeight, not both. */
|
||||
maxHeight: z
|
||||
.union([z.number(), z.undefined()])
|
||||
.optional()
|
||||
.describe(
|
||||
"Maximum available viewport height in pixels (if constrained). Only pass height or maxHeight, not both.",
|
||||
),
|
||||
.describe("Window viewport height in pixels."),
|
||||
})
|
||||
.optional()
|
||||
.describe("Current and maximum dimensions available to the UI."),
|
||||
.describe(
|
||||
"Window viewport dimensions. Represents the host window's viewport size,\nwhich provides additional information apps can use to make responsive layout decisions.",
|
||||
),
|
||||
/** @description User's language and region preference in BCP 47 format. */
|
||||
locale: z
|
||||
.string()
|
||||
|
|
|
|||
|
|
@ -325,16 +325,39 @@ export interface McpUiHostContext {
|
|||
displayMode?: McpUiDisplayMode;
|
||||
/** @description Display modes the host supports. */
|
||||
availableDisplayModes?: string[];
|
||||
/** @description Current and maximum dimensions available to the UI. */
|
||||
/**
|
||||
* @description Container dimensions. Represents the dimensions of the iframe or other
|
||||
* container holding the app. Specify either width or maxWidth, and either height or maxHeight.
|
||||
*/
|
||||
containerDimensions?: (
|
||||
| {
|
||||
/** @description Fixed container height in pixels. */
|
||||
height: number;
|
||||
}
|
||||
| {
|
||||
/** @description Maximum container height in pixels. */
|
||||
maxHeight?: number | undefined;
|
||||
}
|
||||
) &
|
||||
(
|
||||
| {
|
||||
/** @description Fixed container width in pixels. */
|
||||
width: number;
|
||||
}
|
||||
| {
|
||||
/** @description Maximum container width in pixels. */
|
||||
maxWidth?: number | undefined;
|
||||
}
|
||||
);
|
||||
/**
|
||||
* @description Window viewport dimensions. Represents the host window's viewport size,
|
||||
* which provides additional information apps can use to make responsive layout decisions.
|
||||
*/
|
||||
viewport?: {
|
||||
/** @description Viewport width in pixels (if fixed). Only pass width or maxWidth, not both. */
|
||||
/** @description Window viewport width in pixels. */
|
||||
width?: number | undefined;
|
||||
/** @description Viewport height in pixels (if fixed). Only pass height or maxHeight, not both. */
|
||||
/** @description Window viewport height in pixels. */
|
||||
height?: number | undefined;
|
||||
/** @description Maximum available viewport width in pixels (if constrained). Only pass width or maxWidth, not both.*/
|
||||
maxWidth?: number | undefined;
|
||||
/** @description Maximum available viewport height in pixels (if constrained). Only pass height or maxHeight, not both. */
|
||||
maxHeight?: number | undefined;
|
||||
};
|
||||
/** @description User's language and region preference in BCP 47 format. */
|
||||
locale?: string;
|
||||
|
|
|
|||
Loading…
Reference in a new issue