From 2fa243ae8c4472b68b227d4ac8ee0322b5fa3faf Mon Sep 17 00:00:00 2001 From: martinalong Date: Fri, 12 Dec 2025 22:15:50 -0800 Subject: [PATCH] Updates example to use theme colors and adds border radius --- .../src/mcp-app.css | 99 ++++++++++--------- .../src/mcp-app.ts | 99 ++++++++++++++++--- specification/draft/apps.mdx | 90 ++++++++++++----- src/generated/schema.json | 20 ++-- 4 files changed, 211 insertions(+), 97 deletions(-) diff --git a/examples/customer-segmentation-server/src/mcp-app.css b/examples/customer-segmentation-server/src/mcp-app.css index 4bdede2..313b158 100644 --- a/examples/customer-segmentation-server/src/mcp-app.css +++ b/examples/customer-segmentation-server/src/mcp-app.css @@ -1,42 +1,45 @@ +/* Default fallback values for host style variables */ :root { - --color-bg: #ffffff; - --color-text: #1f2937; - --color-text-muted: #6b7280; - --color-primary: #2563eb; - --color-primary-hover: #1d4ed8; - --color-card-bg: #f9fafb; - --color-border: #e5e7eb; - --color-enterprise: #1e40af; - --color-midmarket: #0d9488; - --color-smb: #059669; - --color-startup: #6366f1; -} + color-scheme: light dark; -@media (prefers-color-scheme: dark) { - :root { - --color-bg: #111827; - --color-text: #f9fafb; - --color-text-muted: #9ca3af; - --color-primary: #3b82f6; - --color-primary-hover: #60a5fa; - --color-card-bg: #1f2937; - --color-border: #374151; - --color-enterprise: #3b82f6; - --color-midmarket: #14b8a6; - --color-smb: #10b981; - --color-startup: #818cf8; - } + /* Background colors */ + --color-background-primary: light-dark(#ffffff, #111827); + --color-background-secondary: light-dark(#f9fafb, #1f2937); + --color-background-tertiary: light-dark(#f3f4f6, #374151); + + /* Text colors */ + --color-text-primary: light-dark(#1f2937, #f9fafb); + --color-text-secondary: light-dark(#6b7280, #9ca3af); + --color-text-tertiary: light-dark(#9ca3af, #6b7280); + + /* Border colors */ + --color-border-primary: light-dark(#e5e7eb, #374151); + --color-border-secondary: light-dark(#d1d5db, #4b5563); + + /* Accent colors */ + --color-accent-info: light-dark(#2563eb, #3b82f6); + + /* Border radius */ + --border-radius-small: 4px; + --border-radius-medium: 8px; + + /* App-specific colors (not from host) */ + --color-enterprise: light-dark(#1e40af, #3b82f6); + --color-midmarket: light-dark(#0d9488, #14b8a6); + --color-smb: light-dark(#059669, #10b981); + --color-startup: light-dark(#6366f1, #818cf8); } html, body { margin: 0; padding: 0; - background: var(--color-bg); - color: var(--color-text); + color: var(--color-text-primary); overflow: hidden; } .main { + border-radius: var(--border-radius-medium); + background: var(--color-background-primary); width: 100%; height: 600px; margin: 0 auto; @@ -85,21 +88,21 @@ html, body { gap: 6px; font-size: 0.8125rem; font-weight: 500; - color: var(--color-text-muted); + color: var(--color-text-secondary); } .select { padding: 4px 8px; font-size: 0.8125rem; - border: 1px solid var(--color-border); - border-radius: 4px; - background: var(--color-card-bg); - color: var(--color-text); + border: 1px solid var(--color-border-primary); + border-radius: var(--border-radius-small); + background: var(--color-background-secondary); + color: var(--color-text-primary); cursor: pointer; } .select:focus { - outline: 2px solid var(--color-primary); + outline: 2px solid var(--color-accent-info); outline-offset: 1px; } @@ -107,10 +110,10 @@ html, body { .chart-section { flex: 1; min-height: 0; - background: var(--color-card-bg); - border-radius: 8px; + background: var(--color-background-secondary); + border-radius: var(--border-radius-medium); padding: 8px; - border: 1px solid var(--color-border); + border: 1px solid var(--color-border-primary); } .chart-container { @@ -141,13 +144,13 @@ html, body { cursor: pointer; padding: 4px 10px; border-radius: 16px; - border: 1px solid var(--color-border); - background: var(--color-card-bg); + border: 1px solid var(--color-border-primary); + background: var(--color-background-secondary); transition: all 0.15s ease; } .legend-item:hover { - border-color: var(--color-text-muted); + border-color: var(--color-text-secondary); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); } @@ -171,7 +174,7 @@ html, body { } .legend-count { - color: var(--color-text-muted); + color: var(--color-text-secondary); } /* Detail section - ~44px */ @@ -186,15 +189,15 @@ html, body { justify-content: center; gap: 16px; height: 100%; - background: var(--color-card-bg); - border-radius: 6px; + background: var(--color-background-secondary); + border-radius: var(--border-radius-small); padding: 0 12px; - border: 1px solid var(--color-border); + border: 1px solid var(--color-border-primary); font-size: 0.8125rem; } .detail-placeholder { - color: var(--color-text-muted); + color: var(--color-text-secondary); } .detail-name { @@ -203,7 +206,7 @@ html, body { .detail-segment { padding: 2px 8px; - border-radius: 4px; + border-radius: var(--border-radius-small); font-size: 0.75rem; font-weight: 500; color: white; @@ -215,10 +218,10 @@ html, body { .detail-segment.startup { background: var(--color-startup); } .detail-metric { - color: var(--color-text-muted); + color: var(--color-text-secondary); } .detail-metric strong { - color: var(--color-text); + color: var(--color-text-primary); font-weight: 600; } diff --git a/examples/customer-segmentation-server/src/mcp-app.ts b/examples/customer-segmentation-server/src/mcp-app.ts index 757937c..b3a4852 100644 --- a/examples/customer-segmentation-server/src/mcp-app.ts +++ b/examples/customer-segmentation-server/src/mcp-app.ts @@ -1,7 +1,12 @@ /** * @file Customer Segmentation Explorer - interactive scatter/bubble visualization */ -import { App, PostMessageTransport } from "@modelcontextprotocol/ext-apps"; +import { + App, + PostMessageTransport, + applyHostStyles, + applyDocumentTheme, +} from "@modelcontextprotocol/ext-apps"; import { Chart, registerables } from "chart.js"; import "./global.css"; import "./mcp-app.css"; @@ -138,11 +143,40 @@ function buildDatasets(): Chart["data"]["datasets"] { }); } +// Hidden element for resolving CSS color values (reused to avoid DOM thrashing) +let colorResolver: HTMLDivElement | null = null; + +// Resolve a CSS color value (handles light-dark() function) +function resolveColor(cssValue: string, fallback: string): string { + if (!cssValue) return fallback; + // If it's a simple color value, return it directly + if (!cssValue.includes("light-dark(")) return cssValue; + // Create resolver element once and keep it hidden + if (!colorResolver) { + colorResolver = document.createElement("div"); + colorResolver.style.position = "absolute"; + colorResolver.style.visibility = "hidden"; + colorResolver.style.pointerEvents = "none"; + document.body.appendChild(colorResolver); + } + colorResolver.style.color = cssValue; + return getComputedStyle(colorResolver).color || fallback; +} + +// Get colors from CSS variables +function getChartColors(): { textColor: string; gridColor: string } { + const style = getComputedStyle(document.documentElement); + const rawTextColor = style.getPropertyValue("--color-text-secondary").trim(); + const rawGridColor = style.getPropertyValue("--color-border-primary").trim(); + return { + textColor: resolveColor(rawTextColor, "#6b7280"), + gridColor: resolveColor(rawGridColor, "#e5e7eb"), + }; +} + // Initialize Chart.js function initChart(): Chart { - const isDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches; - const textColor = isDarkMode ? "#9ca3af" : "#6b7280"; - const gridColor = isDarkMode ? "#374151" : "#e5e7eb"; + const { textColor, gridColor } = getChartColors(); return new Chart(chartCanvas, { type: "bubble", @@ -243,8 +277,7 @@ function initChart(): Chart { function updateChart(): void { if (!state.chart) return; - const isDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches; - const textColor = isDarkMode ? "#9ca3af" : "#6b7280"; + const { textColor, gridColor } = getChartColors(); state.chart.data.datasets = buildDatasets(); @@ -252,11 +285,13 @@ function updateChart(): void { const scales = state.chart.options.scales as { x: { title: { text: string; color: string }; - ticks: { callback: (value: number) => string }; + ticks: { color: string; callback: (value: number) => string }; + grid: { color: string }; }; y: { title: { text: string; color: string }; - ticks: { callback: (value: number) => string }; + ticks: { color: string; callback: (value: number) => string }; + grid: { color: string }; }; }; @@ -264,8 +299,12 @@ function updateChart(): void { scales.y.title.text = METRIC_LABELS[state.yAxis]; scales.x.title.color = textColor; scales.y.title.color = textColor; + scales.x.ticks.color = textColor; + scales.y.ticks.color = textColor; scales.x.ticks.callback = (value: number) => formatValue(value, state.xAxis); scales.y.ticks.callback = (value: number) => formatValue(value, state.yAxis); + scales.x.grid.color = gridColor; + scales.y.grid.color = gridColor; state.chart.update(); } @@ -388,20 +427,52 @@ document.addEventListener("click", (e) => { } }); -// Handle theme changes +// Handle system theme changes (fallback when host doesn't provide styles) window .matchMedia("(prefers-color-scheme: dark)") - .addEventListener("change", () => { - if (state.chart) { - state.chart.destroy(); - state.chart = initChart(); + .addEventListener("change", (e) => { + // Only apply if we haven't received host theme + if (!app.getHostContext()?.theme) { + applyDocumentTheme(e.matches ? "dark" : "light"); + if (state.chart) { + state.chart.destroy(); + state.chart = initChart(); + } } }); +// Apply initial theme based on system preference (before host context is available) +const systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches; +applyDocumentTheme(systemDark ? "dark" : "light"); + // Register handlers and connect app.onerror = log.error; -app.connect(new PostMessageTransport(window.parent)); +// Handle host context changes (theme and styles from host) +app.onhostcontextchanged = (params) => { + if (params.theme) { + applyDocumentTheme(params.theme); + } + if (params.styles) { + applyHostStyles(params.styles); + } + // Recreate chart to pick up new colors + if (state.chart && (params.theme || params.styles)) { + state.chart.destroy(); + state.chart = initChart(); + } +}; + +app.connect(new PostMessageTransport(window.parent)).then(() => { + // Apply initial host context after connection + const ctx = app.getHostContext(); + if (ctx?.theme) { + applyDocumentTheme(ctx.theme); + } + if (ctx?.styles) { + applyHostStyles(ctx.styles); + } +}); // Fetch data after connection setTimeout(fetchData, 100); diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index 2586d52..d4eb6d9 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -154,11 +154,12 @@ interface UIResourceMeta { /** * Visual boundary preference * - * Boolean indicating the UI prefers a visible border. Useful for widgets - * that might blend with host background. + * Boolean controlling whether a visible border and background is provided by the host. Specifying an + * explicit value for this is recommended because hosts' defaults may vary. * - * - `true`: Request visible border (host decides styling) - * - `false` or omitted: No preference + * - `true`: Request visible border + background + * - `false`: Request no visible border + background + * - omitted: host decides border */ prefersBorder?: boolean, } @@ -391,9 +392,8 @@ UI iframes can use the following subset of standard MCP protocol messages: ### Host Context in `McpUiInitializeResult` -When the Guest UI sends an `ui/initialize` request to the Host, the Host SHOULD include UI-specific context in the `McpUiInitializeResult`’s `hostContext` field: +When the Guest UI sends an `ui/initialize` request to the Host, the Host SHOULD include UI-specific context in the `McpUiInitializeResult`'s `hostContext` field: -```typescript interface HostContext { /** Metadata of the tool call that instantiated the App */ toolInfo?: { @@ -405,7 +405,7 @@ interface HostContext { /** Current color theme preference */ theme?: "light" | "dark"; /** CSS variables for theming. See Theming section for standardized variable names. */ - styles?: Record; + styles?: Record; /** How the UI is currently displayed */ displayMode?: "inline" | "fullscreen" | "pip"; /** Display modes the host supports */ @@ -472,32 +472,72 @@ Hosts can optionally pass CSS custom properties via `HostContext.styles` for vis #### Current Standardized Variables -| Category | Variables | -|----------|-----------| -| Background | `--color-background-primary`, `--color-background-secondary`, `--color-background-tertiary`, `--color-background-inverted` | -| Text | `--color-text-primary`, `--color-text-secondary`, `--color-text-tertiary`, `--color-text-inverted` | -| Icons | `--color-icon-primary`, `--color-icon-secondary`, `--color-icon-tertiary`, `--color-icon-inverted` | -| Borders | `--color-border-primary`, `--color-border-secondary` | -| Accents | `--color-accent-info`, `--color-accent-danger`, `--color-accent-success`, `--color-accent-warning` | -| Font Family | `--font-family-sans` | -| Font Sizes | `--font-size-heading`, `--font-size-body`, `--font-size-caption` | -| Font Weights | `--font-weight-regular`, `--font-weight-emphasized` | -| Line Heights | `--font-leading-regular`, `--font-leading-tight` | -| Composite Styles | `--font-style-heading`, `--font-style-body`, `--font-style-body-emphasized`, `--font-style-caption`, `--font-style-caption-emphasized` | -| Border Radius | `--border-radius-small`, `--border-radius-medium`, `--border-radius-large`, `--border-radius-full` | -| Border Width | `--border-width-regular` | +```typescript +/** CSS variable keys available to MCP apps for theming. */ +type McpUiStyleVariableKey = + // Background colors + | "--color-background-primary" + | "--color-background-secondary" + | "--color-background-tertiary" + | "--color-background-inverted" + // Text colors + | "--color-text-primary" + | "--color-text-secondary" + | "--color-text-tertiary" + | "--color-text-inverted" + // Icon colors + | "--color-icon-primary" + | "--color-icon-secondary" + | "--color-icon-tertiary" + | "--color-icon-inverted" + // Border colors + | "--color-border-primary" + | "--color-border-secondary" + // Accent colors + | "--color-accent-info" + | "--color-accent-danger" + | "--color-accent-success" + | "--color-accent-warning" + // Typography - Family + | "--font-family-sans" + // Typography - Size + | "--font-size-heading" + | "--font-size-body" + | "--font-size-caption" + // Typography - Weight + | "--font-weight-regular" + | "--font-weight-emphasized" + // Typography - Line height + | "--font-leading-regular" + | "--font-leading-tight" + // Typography - Composite styles + | "--font-style-heading" + | "--font-style-body" + | "--font-style-body-emphasized" + | "--font-style-caption" + | "--font-style-caption-emphasized" + // Border radius + | "--border-radius-small" + | "--border-radius-medium" + | "--border-radius-large" + | "--border-radius-full" + // Border width + | "--border-width-regular"; #### Host Behavior -- Hosts can provide any subset of standardized variables, or not pass `styles` at all. However, passing all of the standardized properties is recommended for cohesiveness -- Hosts can use the CSS `light-dark()` function for theme-aware values +- Hosts can provide any subset of standardized variables, or not pass `styles` at all + - However, unexpected clashes may occur if hosts pass some color variables but not others for example, since apps are instructed to fallback on their own default values for unspecified style variables +- Hosts should use the CSS `light-dark()` function for theme-aware values (i.e. light mode and dark mode colors) #### App Behavior -- Apps should set default fallback values for CSS variables, to account for hosts who don't pass some or all style variables. This ensures graceful degradation when hosts omit `styles` or specific variables: +- Apps should set default fallback values for the set of these variables that they use, to account for hosts who don't pass some or all style variables. This ensures graceful degradation when hosts omit `styles` or specific variables: ``` :root { - --colorTextPrimary: #171717; + --color-text-primary: light-dark(#171717, #000000); + --border-radius-small: 8px; + ... } ``` - Apps can use the `applyHostStyles` utility (or `useHostStyles` if they prefer a React hook) to easily populate the host-provided CSS variables into their style sheet diff --git a/src/generated/schema.json b/src/generated/schema.json index 047ad29..6a54ce0 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -1698,7 +1698,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" } }, "additionalProperties": false @@ -1745,7 +1745,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" } }, "additionalProperties": false @@ -1792,7 +1792,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" } }, "additionalProperties": false @@ -1866,7 +1866,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" } }, "additionalProperties": false @@ -1959,7 +1959,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" } }, "additionalProperties": false @@ -2670,7 +2670,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" } }, "additionalProperties": false @@ -2717,7 +2717,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" } }, "additionalProperties": false @@ -2764,7 +2764,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" } }, "additionalProperties": false @@ -2838,7 +2838,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" } }, "additionalProperties": false @@ -2931,7 +2931,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" } }, "additionalProperties": false