Files
networkSetup/output/msix-builder-preview.html
T
jychoi4 24b624301c docs: define MSIX-first packaging design and roadmap
Document package formats, release workflows, deployment planning and UI concepts.

Closes #1
2026-10-27 16:30:00 +09:00

2836 lines
116 KiB
HTML

<!doctype html>
<html lang="en" data-visualize-standalone>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="referrer" content="no-referrer">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' blob: data: https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh https://fonts.bunny.net https://fonts.googleapis.com https://fonts.gstatic.com https://unpkg.com; style-src 'unsafe-inline' blob: data: https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh https://fonts.bunny.net https://fonts.googleapis.com https://fonts.gstatic.com https://unpkg.com; img-src blob: data: https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh https://fonts.bunny.net https://fonts.googleapis.com https://fonts.gstatic.com https://unpkg.com; font-src blob: data: https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh https://fonts.bunny.net https://fonts.googleapis.com https://fonts.gstatic.com https://unpkg.com; media-src blob: data: https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh https://fonts.bunny.net https://fonts.googleapis.com https://fonts.gstatic.com https://unpkg.com; worker-src blob:; connect-src blob: data:; frame-src 'self'; object-src 'none'; base-uri 'none'; form-action 'none'">
<title>Msix Builder Concept</title>
<style>:root{color-scheme:light dark;background:light-dark(rgb(255 255 255), rgb(24 24 24))}html,body{margin:0}body{box-sizing:border-box;padding:1rem;background:inherit}iframe{display:block;width:100%;height:calc(100vh - 2rem);margin:0 auto;border:0}</style>
</head>
<body>
<iframe id="codex-visualization" sandbox="allow-scripts" referrerpolicy="no-referrer" title="Msix Builder Concept" data-srcdoc="&lt;!doctype html&gt;
&lt;html lang=&quot;en&quot; data-visualize-standalone&gt;
&lt;head&gt;
&lt;meta charset=&quot;utf-8&quot;&gt;
&lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot;&gt;
&lt;meta name=&quot;referrer&quot; content=&quot;no-referrer&quot;&gt;
&lt;meta http-equiv=&quot;Content-Security-Policy&quot; content=&quot;default-src &#x27;none&#x27;; script-src &#x27;unsafe-inline&#x27; &#x27;unsafe-eval&#x27; &#x27;wasm-unsafe-eval&#x27; blob: data: https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh https://fonts.bunny.net https://fonts.googleapis.com https://fonts.gstatic.com https://unpkg.com; style-src &#x27;unsafe-inline&#x27; blob: data: https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh https://fonts.bunny.net https://fonts.googleapis.com https://fonts.gstatic.com https://unpkg.com; img-src blob: data: https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh https://fonts.bunny.net https://fonts.googleapis.com https://fonts.gstatic.com https://unpkg.com; font-src blob: data: https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh https://fonts.bunny.net https://fonts.googleapis.com https://fonts.gstatic.com https://unpkg.com; media-src blob: data: https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh https://fonts.bunny.net https://fonts.googleapis.com https://fonts.gstatic.com https://unpkg.com; worker-src blob:; connect-src blob: data:; frame-src &#x27;none&#x27;; object-src &#x27;none&#x27;; base-uri &#x27;none&#x27;; form-action &#x27;none&#x27;&quot;&gt;
&lt;title&gt;Msix Builder Concept&lt;/title&gt;
&lt;style&gt;:root {
color-scheme: light dark;
background-color: var(
--background,
var(--color-background-primary, light-dark(rgb(255 255 255), rgb(24 24 24)))
) !important;
/* Agent-facing contract; keep in sync with SKILL.md. */
--background: var(
--color-background-primary,
light-dark(rgb(255 255 255), rgb(24 24 24))
);
--foreground: var(
--color-text-primary,
light-dark(rgb(26 28 31), rgb(255 255 255))
);
--card: color-mix(in oklab, var(--foreground) 5%, var(--background));
--card-foreground: var(
--color-text-primary,
light-dark(rgb(26 28 31), rgb(255 255 255))
);
--popover: var(
--color-background-secondary,
light-dark(rgb(255 255 255), rgb(45 45 45))
);
--popover-foreground: var(
--color-text-primary,
light-dark(rgb(26 28 31), rgb(255 255 255))
);
--primary: var(
--color-text-info,
light-dark(rgb(51 156 255), rgb(131 195 255))
);
--primary-foreground: var(
--color-text-inverse,
light-dark(rgb(255 255 255), rgb(13 13 13))
);
--secondary: var(
--color-background-secondary,
light-dark(rgb(255 255 255 / 96%), rgb(54 54 54 / 96%))
);
--secondary-foreground: var(
--color-text-primary,
light-dark(rgb(26 28 31), rgb(255 255 255))
);
--muted: color-mix(in srgb, var(--foreground) 10%, transparent);
--muted-foreground: var(
--color-text-secondary,
light-dark(rgb(26 28 31 / 49.4%), rgb(255 255 255 / 49.8%))
);
--accent: var(
--color-background-info,
light-dark(rgb(229 242 255), rgb(13 39 63))
);
--accent-foreground: var(
--color-text-info,
light-dark(rgb(51 156 255), rgb(131 195 255))
);
--destructive: var(
--color-text-warning,
light-dark(rgb(226 85 7), rgb(255 133 73))
);
--border: var(
--color-border-secondary,
light-dark(rgb(26 28 31 / 8%), rgb(255 255 255 / 8.2%))
);
--input: var(
--color-border-primary,
light-dark(
rgb(26 28 31 / 11.8%),
color-mix(in oklab, rgb(0 0 0) 10%, transparent)
)
);
--ring: var(
--color-ring-primary,
light-dark(rgb(51 156 255), rgb(131 195 255 / 76%))
);
--font-size-base: var(--font-text-md-size, 16px);
--blue: light-dark(rgb(51 156 255), rgb(51 156 255));
--orange: light-dark(rgb(226 85 7), rgb(251 106 34));
--green: light-dark(rgb(0 162 64), rgb(64 201 119));
--red: light-dark(rgb(224 46 42), rgb(255 103 100));
--purple: light-dark(rgb(146 79 247), rgb(173 123 249));
--yellow: light-dark(rgb(255 195 0), rgb(255 210 64));
--viz-series-1: var(--primary);
--viz-series-2: light-dark(rgb(243 136 59), rgb(245 154 86));
--viz-series-3: light-dark(rgb(93 201 119), rgb(116 213 139));
--viz-series-4: light-dark(rgb(235 119 177), rgb(240 143 192));
--viz-series-5: light-dark(rgb(155 121 236), rgb(170 145 239));
--viz-series-6: light-dark(rgb(58 185 177), rgb(90 203 194));
/* Internal implementation variables; not part of the agent contract. */
--font-sans:
-apple-system-body, ui-sans-serif, -apple-system, system-ui, &quot;Segoe UI&quot;,
Helvetica, &quot;Apple Color Emoji&quot;, Arial, sans-serif, &quot;Segoe UI Emoji&quot;,
&quot;Segoe UI Symbol&quot;;
--font-mono:
ui-monospace, SFMono-Regular, &quot;SF Mono&quot;, Menlo, Consolas, &quot;Liberation Mono&quot;,
monospace;
--font-size-normal: max(11px, var(--font-size-base));
--font-size-tooltip: calc(var(--font-size-base) - 1px);
--font-size-small: max(11px, calc(var(--font-size-base) - 2px));
--font-size-h1: calc(var(--font-size-normal) * 1.7142857143);
--font-size-h2: calc(var(--font-size-normal) * 1.4285714286);
--font-size-h3: calc(var(--font-size-normal) * 1.2857142857);
--font-weight-normal: 400;
--font-weight-medium: 500;
--icon-size: 16px;
--line-height-normal: calc(var(--font-size-normal) * 1.5);
--line-height-tooltip: calc(var(--font-size-tooltip) * 1.4285714286);
--line-height-small: calc(var(--font-size-small) + 4px);
--corner-shape: round;
--radius: var(--border-radius-lg, 8px);
--radius-sm: calc(var(--radius) * 0.5);
--radius-md: calc(var(--radius) * 0.75);
--radius-lg: var(--radius);
--radius-2xl: calc(var(--radius) * 2);
--radius-full: 9999px;
--shadow-sm: 0 1px 2px -1px rgb(0 0 0 / 8%);
--checkmark-image: url(&quot;data:image/svg+xml,%3Csvg xmlns=&#x27;http://www.w3.org/2000/svg&#x27; viewBox=&#x27;0 0 17 17&#x27;%3E%3Cpath d=&#x27;M12.8961 3.64101C13.1297 3.41418 13.4984 3.37523 13.7779 3.56581C14.0571 3.75635 14.1554 4.11331 14.0299 4.41347L13.9615 4.53847L7.71151 13.7045C7.59411 13.8767 7.4063 13.9877 7.19881 14.0072C6.99136 14.0267 6.78564 13.9533 6.63826 13.806L2.88826 10.056L2.79842 9.9457C2.6192 9.67407 2.64927 9.30496 2.88826 9.06581C3.12738 8.82669 3.49647 8.79676 3.76815 8.97597L3.8785 9.06581L7.03084 12.2182L12.8053 3.74941L12.8961 3.64101Z&#x27;/%3E%3C/svg%3E&quot;);
/* Legacy aliases; not part of the current agent contract. */
--viz-bg: transparent;
--viz-panel: var(--card);
--viz-border: var(--border);
--viz-text: var(--foreground);
--viz-muted: var(--muted-foreground);
--viz-accent: var(--primary);
--viz-accent-text: var(--primary-foreground);
--viz-accent-bg: var(--accent);
--viz-font-size: var(--font-size-base);
--viz-warning: var(--destructive);
}
:root[data-theme=&quot;light&quot;] {
color-scheme: light;
}
:root[data-theme=&quot;dark&quot;] {
color-scheme: dark;
}
* {
box-sizing: border-box;
}
html &gt; body {
/* Preserve MCP aliases for older inline renderers without creating :root cycles. */
--color-background-primary: var(
--background,
light-dark(rgb(255 255 255), rgb(24 24 24))
);
--color-text-primary: var(
--foreground,
light-dark(rgb(26 28 31), rgb(255 255 255))
);
--color-border-secondary: var(
--border,
light-dark(rgb(26 28 31 / 8%), rgb(255 255 255 / 8.2%))
);
margin: 0;
padding: var(--visualize-paint-gutter, 0px);
color: var(--foreground);
background: transparent !important;
font-family: var(--font-sans) !important;
-webkit-font-smoothing: antialiased;
font-size: var(--font-size-base);
line-height: calc(var(--font-size-base) * 1.625);
font-weight: var(--font-weight-normal);
}
.cursor-interaction {
cursor: var(--cursor-interaction, pointer);
}
a {
color: color-mix(in srgb, var(--viz-accent) 80%, var(--viz-text) 20%);
cursor: var(--cursor-interaction, pointer);
text-decoration: none;
text-underline-offset: 2px;
font-weight: inherit;
}
a:is(:hover, :focus-visible) {
text-decoration-line: underline;
text-decoration-style: dashed;
text-decoration-thickness: 0.5px;
}
h1,
h2,
h3,
h4,
h5,
h6,
p {
margin-block: 0;
}
p,
h1,
h2,
h3,
h4,
h5,
h6,
li,
.table {
font-family: var(--font-sans);
}
h1 {
font-size: calc(var(--font-size-base) * 1.5);
line-height: 1.3333333333;
}
h2 {
font-size: calc(var(--font-size-base) * 1.25);
line-height: 1.4;
}
h3 {
font-size: calc(var(--font-size-base) * 1.125);
line-height: 1.5555555556;
}
h4 {
font-size: var(--font-size-base);
line-height: 1.5;
}
h5,
h6 {
font-size: var(--font-size-base);
line-height: calc(var(--font-size-base) * 1.625);
}
h1,
h2,
h3,
h4,
h5,
h6,
b,
strong,
th {
font-weight: 600;
}
code:not(pre code) {
display: inline;
padding: 1px 6px;
border-radius: var(--radius-sm);
corner-shape: var(--corner-shape);
background: var(--muted);
font-family: var(--font-mono);
font-size: 0.92em;
overflow-wrap: anywhere;
-webkit-box-decoration-break: clone;
box-decoration-break: clone;
word-break: break-word;
}
pre,
pre code {
font-family: var(--font-mono);
font-size: 12px;
line-height: 20px;
}
.table-responsive {
width: 100%;
overflow-x: auto;
scrollbar-width: thin;
}
.table {
width: 100%;
color: var(--foreground);
font: inherit;
text-align: start;
border-collapse: separate;
border-spacing: 0;
font-size: calc(var(--font-size-base) * 0.875);
line-height: calc(var(--font-size-base) * 1.625);
font-family: var(--font-sans);
}
.table :is(th, td) {
padding-block: 10px;
padding-inline: 0 24px;
overflow-wrap: anywhere;
border-bottom: 1px solid var(--border);
text-align: start;
vertical-align: top;
border-bottom-color: color-mix(in srgb, var(--foreground) 5%, transparent);
}
.table :is(th, td):not(:last-child) {
padding-inline-end: calc(var(--font-size-base) * 1.5);
}
.table-responsive &gt; .table :is(th, td) {
overflow-wrap: break-word;
}
.table :is(th, td):last-child {
padding-inline-end: 0;
}
.table :is(caption, thead th) {
font-weight: 600;
}
.table thead :is(th, td) {
border-bottom-color: var(--border-strong, var(--input));
padding-block: calc(var(--font-size-base) / 2);
line-height: var(--font-size-base);
}
.table tbody tr:last-child :is(th, td) {
border-bottom: 0;
}
.table tbody :is(th, td) {
padding-block: calc(var(--font-size-base) * 0.625);
}
.table.table-sm :is(th, td) {
padding-block: 8px;
}
.table.table-sm :is(th, td):not(:last-child) {
padding-inline-end: 16px;
}
.table :is(.text-end, [align=&quot;right&quot;]) {
text-align: end;
font-variant-numeric: tabular-nums;
}
.table :is(.text-center, [align=&quot;center&quot;]) {
text-align: center;
}
.table .text-nowrap {
white-space: nowrap;
}
#widget {
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
padding: 0;
background: transparent !important;
}
.card {
min-width: 0;
padding: 12px;
overflow: hidden;
overflow-wrap: break-word;
border-radius: var(--radius-2xl);
corner-shape: var(--corner-shape);
color: var(--card-foreground);
background: var(--card);
}
.widget {
min-width: 0;
overflow: hidden;
border: 0;
border-radius: var(--radius-lg);
corner-shape: var(--corner-shape);
box-shadow: 0 0 0 0.5px var(--border-strong, var(--border));
color: var(--foreground);
background: var(--background);
}
#widget &gt; :not(.card, .widget, .progress) {
width: 100% !important;
max-width: none !important;
margin: 0 !important;
padding: 0 !important;
border: 0 !important;
border-radius: 0 !important;
background: transparent !important;
box-shadow: none !important;
}
.tooltip {
position: fixed;
z-index: 50;
top: 0;
left: 0;
width: max-content;
max-width: min(
20rem,
var(--tooltip-available-width, calc(100vw - 10px)),
calc(100vw - 10px)
);
max-height: min(
var(--tooltip-available-height, calc(100vh - 10px)),
calc(100vh - 10px)
);
border: 1px solid var(--border);
corner-shape: var(--corner-shape);
overflow-wrap: break-word;
white-space: normal;
pointer-events: none;
user-select: none;
font-family: var(--font-sans);
padding: 5px 12px;
border-color: color-mix(
in oklab,
light-dark(var(--primary-foreground), var(--foreground)) 5%,
transparent
);
border-radius: var(--radius-2xl);
color: light-dark(var(--primary-foreground), var(--foreground));
background: light-dark(var(--foreground), var(--popover));
box-shadow: 0 8px 18px rgb(15 23 42 / 20%);
letter-spacing: -0.15px;
font-size: calc(var(--font-size-base) * 14 / 16);
font-size: round(nearest, calc(var(--font-size-base) * 14 / 16), 1px);
font-weight: 600;
line-height: 18px;
text-align: center;
}
.viz-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(max(180px, 24%), 1fr));
gap: 10px;
}
.viz-stat {
display: flex;
flex-direction: column;
gap: 2px;
}
.viz-stat-value {
font-size: var(--font-size-h2);
font-weight: var(--font-weight-medium);
line-height: 1.25;
}
.viz-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
}
hr {
width: 100%;
height: 1px;
margin-block: 6px;
border: 0;
background: var(--border);
}
.nav {
display: flex;
min-width: 0;
align-items: center;
gap: 4px;
}
.progress {
display: flex;
height: 8px;
margin-block: calc((var(--line-height-normal, 1.5em) - 8px) / 2);
overflow: hidden;
border: 0;
border-radius: var(--radius-full);
background: var(--muted);
}
.progress-bar:only-child {
border-radius: var(--radius-full);
}
.progress-bar {
height: 100%;
flex-shrink: 0;
border: 0;
background: var(--viz-series-1);
}
.viz-badge {
display: inline-flex;
align-items: center;
padding: 4px 8px;
border-radius: var(--radius-sm);
color: color-mix(in srgb, var(--foreground) 40%, var(--muted-foreground));
background: color-mix(in srgb, var(--foreground) 5%, transparent);
font-family: var(--font-sans);
font-size: calc(var(--font-size-base) * 14 / 16);
font-size: round(nearest, calc(var(--font-size-base) * 14 / 16), 1px);
font-weight: var(--font-weight-normal);
line-height: 1;
}
small,
.text-small {
font-size: var(--font-size-small);
line-height: var(--line-height-small);
}
.text-muted {
color: var(--muted-foreground);
}
.tabular-nums {
font-variant-numeric: tabular-nums;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.viz-controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.viz-controls &gt; .form-label {
display: grid;
min-width: min(100%, 260px);
flex: 1 1 280px;
grid-template-columns: minmax(0, 1fr) auto;
align-items: baseline;
gap: 2px 12px;
margin-bottom: 0;
}
.viz-controls &gt; .form-label &gt; :is(.form-control, .form-range, .form-select) {
grid-column: 1 / -1;
}
.btn,
.nav-link,
.form-check-input,
.form-control,
.form-range,
.form-select {
font: inherit;
}
/* Match inline alignment without overriding a containing flex or grid layout. */
.btn,
.nav-link,
.form-control,
.form-select,
.form-range,
.form-check {
vertical-align: middle;
}
.btn,
.nav-link {
appearance: button;
display: inline-flex;
inline-size: fit-content;
max-inline-size: 100%;
align-items: center;
justify-content: center;
gap: 4px;
margin: 0;
padding: 0 8px;
-webkit-app-region: no-drag;
border: 1px solid var(--input);
corner-shape: var(--corner-shape);
cursor: var(--cursor-interaction, pointer);
text-align: center;
text-decoration: none;
white-space: nowrap;
user-select: none;
-webkit-font-smoothing: inherit;
font-family: var(--font-sans);
height: fit-content;
min-height: calc(12px + 2 * var(--control-spacing, 12px));
padding-inline: var(--control-spacing, 12px);
border-color: transparent;
border-radius: var(--radius-full);
color: var(--foreground);
background: color-mix(in srgb, var(--foreground) 5%, transparent);
font-size: calc(var(--font-size-base) * 14 / 16);
font-size: round(nearest, calc(var(--font-size-base) * 14 / 16), 1px);
font-weight: var(--font-weight-normal);
line-height: 18px;
}
.btn {
border-color: var(--border);
color: var(--secondary-foreground);
background: var(--secondary);
}
.btn:is(.btn-block, .viz-tile) {
inline-size: 100%;
}
.btn.viz-tile {
min-width: 0;
overflow-wrap: anywhere;
white-space: normal;
}
a.btn {
cursor: var(--cursor-interaction, pointer);
text-decoration: none;
}
.btn:not(:disabled):hover {
background: light-dark(
color-mix(in srgb, var(--foreground) 5.5%, transparent),
color-mix(in srgb, var(--foreground) 8%, transparent)
);
}
.nav-link:not(:disabled):not([aria-disabled=&quot;true&quot;]):hover {
background: color-mix(in srgb, var(--foreground) 10%, transparent);
}
.btn-primary {
border-color: var(--border);
color: var(--primary-foreground);
background: var(--foreground);
background-clip: border-box;
}
.btn-primary .text-muted {
color: color-mix(in srgb, var(--primary-foreground) 50%, transparent);
}
.btn-primary:not(:disabled):hover {
background: color-mix(in srgb, var(--foreground) 80%, transparent);
background-clip: border-box;
}
.btn-ghost {
border-color: transparent;
color: var(--muted-foreground);
background: transparent;
}
.btn-ghost:not(:disabled):hover {
color: var(--muted-foreground);
background: light-dark(
color-mix(in srgb, var(--foreground) 5.5%, transparent),
color-mix(in srgb, var(--foreground) 8%, transparent)
);
}
.nav-pills .nav-link {
min-width: 0;
border-color: transparent;
color: var(--muted-foreground);
background: transparent;
}
.nav-justified .nav-link {
flex: 1 1 0;
overflow-wrap: anywhere;
white-space: normal;
}
.nav-pills .nav-link.active {
color: var(--foreground);
background: var(--muted);
}
/* Shared preview chrome; product styles stay inside each variant. */
.viz-dotted-background {
background-image: radial-gradient(
circle,
color-mix(in srgb, var(--foreground) 14%, transparent) 0.8px,
transparent 0.8px
);
background-position: center;
background-size: 16px 16px;
}
.viz-carousel {
position: relative;
min-width: 0;
}
.viz-carousel:has(&gt; .viz-carousel-controls) {
min-height: calc(60px + var(--viz-carousel-picker-height, 0px));
padding-bottom: 60px;
}
.viz-carousel &gt; [data-variant][hidden] {
display: none !important;
}
.viz-carousel &gt; [data-variant] {
isolation: isolate;
}
.viz-carousel-controls {
--viz-carousel-hover: light-dark(
color-mix(in srgb, var(--foreground) 5.4%, transparent),
color-mix(in srgb, var(--foreground) 7.8%, transparent)
);
position: absolute;
z-index: 2;
bottom: 8px;
left: 50%;
display: flex;
width: max-content;
/* Leave room for the host&#x27;s design-controls launcher at the trailing edge. */
max-width: calc(100% - 96px);
align-items: center;
gap: 0;
padding: 0;
border: 1px solid var(--border);
border-radius: 16px;
color: var(--foreground);
background: var(--background);
font-family: var(--font-sans);
/* Match the settings dropdown chrome independently of the content font size. */
font-size: 13px;
font-weight: var(--font-weight-normal);
line-height: 18px;
transform: translateX(-50%);
user-select: none;
}
/* Keep authored mockup button styles out of the shared carousel chrome. */
.viz-carousel-controls button,
.viz-carousel-picker &gt; summary {
display: flex !important;
height: auto !important;
min-height: 30px;
align-items: center !important;
justify-content: center !important;
gap: 4px;
padding: 0 6px !important;
-webkit-app-region: no-drag;
border: 0 !important;
border-radius: 14px !important;
color: inherit !important;
background: transparent !important;
font: inherit !important;
-webkit-tap-highlight-color: transparent;
}
.viz-carousel-controls &gt; button,
.viz-carousel-picker &gt; summary {
position: relative;
isolation: isolate;
}
.viz-carousel-controls &gt; button {
--icon-size: 16px;
width: 30px !important;
padding: 0 !important;
}
.viz-carousel-picker &gt; summary {
padding-inline: 12px !important;
}
.viz-carousel-controls .viz-carousel-caret {
width: 14px;
height: 14px;
flex: none;
margin: 0;
color: inherit;
transform: rotate(180deg);
}
.viz-carousel-controls &gt; button &gt; svg,
.viz-carousel-controls .viz-carousel-caret,
.viz-carousel-count {
opacity: 0.5;
}
.viz-carousel-controls &gt; button::before,
.viz-carousel-picker &gt; summary::before {
position: absolute;
z-index: -1;
inset: 3px;
border-radius: 12px;
content: &quot;&quot;;
pointer-events: none;
}
.viz-carousel-controls &gt; button:hover::before,
.viz-carousel-picker &gt; summary:hover::before {
background: var(--viz-carousel-hover);
}
.viz-carousel-controls &gt; button,
.viz-carousel-controls [data-lucide],
.viz-carousel-controls .lucide {
flex-shrink: 0;
}
.viz-carousel-picker {
min-width: 0;
}
.viz-carousel-picker &gt; summary {
min-width: 0;
list-style: none;
}
.viz-carousel-picker &gt; summary::-webkit-details-marker {
display: none;
}
.viz-carousel-count {
flex-shrink: 0;
white-space: nowrap;
}
.viz-carousel-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.viz-carousel-options {
position: absolute;
right: 0;
bottom: calc(100% + 4px);
left: 0;
display: flex;
max-height: 240px;
flex-direction: column;
gap: 2px;
overflow: auto;
padding: 4px;
border: 1px solid var(--border);
border-radius: var(--radius-lg);
background: var(--background);
box-shadow: 0 4px 12px light-dark(rgb(0 0 0 / 6%), rgb(0 0 0 / 25%));
}
.viz-carousel-options &gt; button {
border-radius: max(0px, calc(var(--radius-lg) - 5px)) !important;
width: 100% !important;
min-height: 28px;
justify-content: flex-start !important;
padding: 4px 8px !important;
font-size: var(--font-size-small) !important;
text-align: start;
white-space: normal;
overflow-wrap: anywhere;
}
.viz-carousel-options &gt; [aria-current=&quot;true&quot;],
.viz-carousel-options &gt; button:hover {
background: var(--viz-carousel-hover) !important;
}
.btn:disabled,
.nav-link:disabled,
.nav-link[aria-disabled=&quot;true&quot;] {
cursor: not-allowed;
opacity: 0.4;
}
[data-lucide] {
stroke-width: 1.6;
}
.form-label {
display: block;
margin-bottom: 6px;
color: var(--foreground);
}
.form-control {
display: block;
width: 100%;
padding: 0 8px;
outline: none;
border: 1px solid var(--input);
corner-shape: var(--corner-shape);
color: var(--foreground);
font-family: var(--font-sans);
height: calc(12px + 2 * var(--control-spacing, 12px));
min-height: calc(12px + 2 * var(--control-spacing, 12px));
padding-inline: calc(4px + var(--control-spacing, 12px) / 2);
border-color: var(--input);
border-radius: var(--radius-md);
background: color-mix(in srgb, var(--popover) 96%, transparent);
font-size: calc(var(--font-size-base) * 14 / 16);
font-size: round(nearest, calc(var(--font-size-base) * 14 / 16), 1px);
font-weight: var(--font-weight-normal);
line-height: 1.4285714286;
}
.form-control:is(
[type=&quot;file&quot;],
[type=&quot;date&quot;],
[type=&quot;datetime-local&quot;],
[type=&quot;month&quot;],
[type=&quot;week&quot;],
[type=&quot;color&quot;]
) {
height: calc(12px + 2 * var(--control-spacing, 12px));
min-height: calc(12px + 2 * var(--control-spacing, 12px));
line-height: 18px;
}
.form-control:is(
[type=&quot;date&quot;],
[type=&quot;datetime-local&quot;],
[type=&quot;month&quot;],
[type=&quot;week&quot;]
) {
padding-inline: var(--control-spacing, 12px);
}
.form-control::placeholder {
color: var(--muted-foreground);
}
.form-control[type=&quot;file&quot;] {
overflow: hidden;
cursor: var(--cursor-interaction, pointer);
padding: 0;
}
.form-control[type=&quot;file&quot;]::file-selector-button {
padding: 0 8px;
border: 0;
border-right: 1px solid var(--input);
color: var(--secondary-foreground);
background: var(--secondary);
cursor: inherit;
font: inherit;
min-height: calc(10px + 2 * var(--control-spacing, 12px));
padding-inline: var(--control-spacing, 12px);
margin-inline-end: var(--control-spacing, 12px);
}
.form-control[type=&quot;file&quot;]:not(:disabled):hover::file-selector-button {
background: color-mix(in srgb, var(--foreground) 6%, var(--secondary));
}
.form-control-color[type=&quot;color&quot;] {
width: 40px;
height: calc(12px + 2 * var(--control-spacing, 12px));
padding: 3px;
cursor: var(--cursor-interaction, pointer);
}
.form-control-color[type=&quot;color&quot;]::-webkit-color-swatch-wrapper {
padding: 0;
}
.form-control-color[type=&quot;color&quot;]::-webkit-color-swatch {
border: 0;
border-radius: calc(var(--radius-lg) - 4px);
corner-shape: var(--corner-shape);
}
textarea.form-control {
height: auto;
resize: vertical;
min-height: 96px;
padding: 8px 10px;
}
.form-control:focus-visible {
border-color: var(--ring);
box-shadow: none;
}
.form-control:disabled {
color: var(--muted-foreground);
background: color-mix(in srgb, var(--foreground) 5%, transparent);
opacity: 1;
}
.form-control[aria-invalid=&quot;true&quot;] {
border-color: var(--red);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--red) 20%, transparent);
}
.form-control:disabled,
.form-select:disabled {
cursor: not-allowed;
}
.form-select {
appearance: none;
display: block;
width: 100%;
height: fit-content;
min-height: calc(12px + 2 * var(--control-spacing, 12px));
margin: 0;
/* The secondary icon role is 14px at compact density and 16px at web density. */
--icon-size: calc(10px + var(--control-spacing, 12px) / 2);
/* Settings dropdowns retain a 12px inset and a 4px text-to-chevron gap. */
padding: 0 calc(16px + var(--icon-size)) 0 12px;
outline: none;
border: 1px solid var(--border);
border-radius: var(--radius-full);
corner-shape: var(--corner-shape);
color: var(--foreground);
background-color: var(--secondary);
background-image:
linear-gradient(
45deg,
transparent calc(50% - 0.6px),
var(--muted-foreground) calc(50% - 0.6px) calc(50% + 0.6px),
transparent calc(50% + 0.6px)
),
linear-gradient(
135deg,
transparent calc(50% - 0.6px),
var(--muted-foreground) calc(50% - 0.6px) calc(50% + 0.6px),
transparent calc(50% + 0.6px)
);
background-position:
calc(100% - 12px - var(--icon-size) / 2) 50%,
calc(100% - 12px - var(--icon-size) * 0.18) 50%;
background-repeat: no-repeat;
background-size: calc(var(--icon-size) * 0.32) calc(var(--icon-size) * 0.32);
cursor: var(--cursor-interaction, default);
font-size: calc(var(--font-size-base) * 14 / 16);
font-size: round(nearest, calc(var(--font-size-base) * 14 / 16), 1px);
font-weight: var(--font-weight-normal);
line-height: 18px;
}
.form-select option {
color: var(--popover-foreground);
background-color: var(--popover);
}
.form-select:not(:disabled):hover {
background-color: light-dark(
color-mix(in srgb, var(--foreground) 5.5%, transparent),
color-mix(in srgb, var(--foreground) 8%, transparent)
);
}
.form-select:focus-visible {
box-shadow: 0 0 0 2px var(--ring);
}
.form-select:disabled {
opacity: 0.4;
}
.form-check {
display: flex;
min-height: calc(12px + 2 * var(--control-spacing, 12px));
align-items: center;
gap: 6px;
}
.form-check-input {
appearance: none;
width: 14px;
height: 14px;
flex: 0 0 auto;
margin: 0;
border: 1px solid var(--input);
color: var(--primary-foreground);
background-color: transparent;
cursor: var(--cursor-interaction, default);
transition:
background-color 150ms,
border-color 150ms,
box-shadow 150ms;
}
.form-check:not(.form-switch) .form-check-input[type=&quot;checkbox&quot;] {
corner-shape: round;
box-shadow: var(--shadow-sm);
position: relative;
width: calc(10px + var(--control-spacing, 12px) / 2);
height: calc(10px + var(--control-spacing, 12px) / 2);
border-color: var(--border-strong, var(--input));
border-radius: calc(var(--radius-sm) / 2);
background-color: transparent;
color: var(--foreground);
}
.form-check:not(.form-switch)
.form-check-input:not(:disabled):not(:checked):hover {
background-color: var(--card);
}
.form-check:not(.form-switch)
.form-check-input[type=&quot;checkbox&quot;]:is(:checked, :indeterminate) {
border-color: var(--border-strong, var(--input));
background-color: color-mix(in srgb, var(--popover) 96%, transparent);
}
.form-check:not(.form-switch)
.form-check-input[type=&quot;checkbox&quot;]:checked::before {
display: block;
width: 100%;
height: 100%;
background: var(--primary-foreground);
content: &quot;&quot;;
mask: var(--checkmark-image) center / 12px 12px no-repeat;
background-color: var(--foreground);
}
.form-check:not(.form-switch)
.form-check-input[type=&quot;checkbox&quot;]:indeterminate::before {
position: absolute;
inset: 0;
display: block;
width: 65%;
height: 6.65%;
margin: auto;
border-radius: var(--radius-full);
background: var(--foreground);
mask: none;
content: &quot;&quot;;
}
.form-check-input[type=&quot;radio&quot;] {
border-radius: var(--radius-full);
width: 16px;
height: 16px;
border: 0;
box-shadow: inset 0 0 0 1px var(--input);
}
.form-check-input[type=&quot;radio&quot;]:checked {
border: 0;
background: radial-gradient(
circle,
var(--primary-foreground) 0 3px,
var(--foreground) 3px
);
box-shadow: none;
}
.form-check:not(.form-switch) .form-check-input:focus-visible {
outline: 2px solid var(--ring);
outline-offset: 2px;
}
.form-check:not(.form-switch) .form-check-input[type=&quot;checkbox&quot;]:focus-visible {
border-color: var(--ring);
outline: none;
box-shadow: 0 0 0 2px var(--ring);
}
.form-check:not(.form-switch) .form-check-input:disabled {
cursor: not-allowed;
pointer-events: none;
}
.form-check:not(.form-switch) .form-check-input:disabled + .form-check-label {
cursor: not-allowed;
}
.form-check:not(.form-switch) .form-check-input[type=&quot;radio&quot;]:disabled,
.form-check:not(.form-switch)
.form-check-input[type=&quot;radio&quot;]:disabled
+ .form-check-label {
opacity: 0.5;
}
.form-switch .form-check-input:disabled,
.form-switch .form-check-input:disabled + .form-check-label {
cursor: not-allowed;
opacity: 0.6;
}
.form-check-label {
color: var(--foreground);
cursor: var(--cursor-interaction, default);
}
.form-switch .form-check-input {
position: relative;
width: 32px;
height: 20px;
border: 0;
border-radius: var(--radius-full);
background: var(--muted);
box-shadow: none;
transition: background-color 200ms cubic-bezier(0, 0, 0.2, 1);
}
.form-switch .form-check-input::before {
position: absolute;
top: 50%;
left: 0;
width: 16px;
height: 16px;
box-sizing: border-box;
border: 1px solid light-dark(var(--primary-foreground), var(--foreground));
border-radius: var(--radius-full);
box-shadow: var(--shadow-sm);
content: &quot;&quot;;
transform: translate(2px, -50%);
transition: transform 200ms cubic-bezier(0, 0, 0.2, 1);
border-color: #fff;
background: #fff;
}
.form-switch .form-check-input:checked {
background: var(--blue);
}
.form-switch .form-check-input:checked::before {
transform: translate(14px, -50%);
}
.form-switch .form-check-input:focus-visible {
box-shadow: 0 0 0 2px var(--ring);
outline: none;
}
.form-range {
appearance: none;
display: block;
width: 100%;
flex: 1;
margin: 0;
padding: 0;
border: 0;
accent-color: var(--primary);
height: calc(12px + 2 * var(--control-spacing, 12px));
min-height: calc(12px + 2 * var(--control-spacing, 12px));
outline: none;
border-radius: var(--radius-full);
color: var(--foreground);
background: transparent;
}
.form-range::-webkit-slider-runnable-track {
background: var(--form-range-track, var(--border));
height: 2px;
border-radius: var(--radius-full);
}
.form-range::-webkit-slider-thumb {
appearance: none;
width: 20px;
height: 20px;
border: 1px solid var(--border);
border-radius: var(--radius-full);
margin-top: -9px;
border-color: transparent;
background: currentColor;
box-shadow: var(--shadow-sm);
}
/* Outline the native track and thumb, preserving the larger input hit area. */
.form-range:focus-visible::-webkit-slider-runnable-track {
outline: 2px auto Highlight;
outline-color: -webkit-focus-ring-color;
}
.form-range::-moz-range-track {
background: var(--form-range-track, var(--border));
height: 2px;
border-radius: var(--radius-full);
}
.form-range::-moz-range-thumb {
width: 20px;
height: 20px;
border: 1px solid var(--border);
border-radius: var(--radius-full);
border-color: transparent;
background: currentColor;
box-shadow: var(--shadow-sm);
}
.form-range:focus-visible::-moz-range-thumb {
outline: 2px solid Highlight;
outline-offset: 2px;
}
.form-range:disabled {
opacity: 1;
cursor: default;
}
.btn:not(.btn-primary, .viz-tile):is(
[aria-pressed=&quot;true&quot;],
[aria-selected=&quot;true&quot;],
.is-selected
) {
border-color: transparent;
color: var(--primary-foreground);
background: var(--blue);
}
.btn.viz-tile:is([aria-pressed=&quot;true&quot;], [aria-selected=&quot;true&quot;], .is-selected) {
border-color: var(--primary);
box-shadow: inset 0 0 0 1px var(--primary);
}
.btn:focus-visible,
.nav-link:focus-visible {
outline-offset: 2px;
outline: none;
box-shadow: 0 0 0 2px var(--ring);
}
.viz-carousel-controls :is(button, summary):focus-visible {
outline: 2px solid var(--ring) !important;
outline-offset: 2px !important;
}
/* Web recipes are the default, including on renderers without inline style queries. */
@supports (border-radius: if(style(--corner-shape: round): 1px; else: 2px)) {
/* Match the host&#x27;s proportional text-sm scale at every text preference. */
.tooltip,
.viz-badge,
.btn,
.nav-link,
.form-control,
.form-select {
font-size: if(
style(--corner-shape: round): round(
nearest,
calc(var(--font-size-base) * 14 / 16),
1px
);
else: round(nearest, calc(var(--font-size-base) * 13 / 14), 1px)
);
}
.btn,
.nav-link,
.form-select {
border-radius: if(
style(--corner-shape: round): var(--radius-full); else: var(--radius-lg)
);
}
.form-check:not(.form-switch) .form-check-input[type=&quot;checkbox&quot;] {
border-radius: if(
style(--corner-shape: round): calc(var(--radius-sm) / 2);
else: calc(var(--radius-lg) * 0.4)
);
}
.tooltip {
font-weight: if(
style(--corner-shape: round): 600; else: var(--font-weight-normal)
);
}
}
@media (pointer: coarse) {
.viz-carousel:has(&gt; .viz-carousel-controls) {
min-height: calc(76px + var(--viz-carousel-picker-height, 0px));
padding-bottom: 76px;
}
.btn,
.nav-link,
.viz-carousel-controls button,
.viz-carousel-picker &gt; summary,
.form-control,
.form-select {
min-width: 44px;
min-height: 44px;
}
/* Keep small-screen WebKit from zooming when a text field receives focus. */
.form-control,
.form-select {
font-size: max(16px, calc(var(--font-size-base) * 14 / 16));
font-size: max(
16px,
round(nearest, calc(var(--font-size-base) * 14 / 16), 1px)
);
}
.form-control:is(
[type=&quot;file&quot;],
[type=&quot;date&quot;],
[type=&quot;datetime-local&quot;],
[type=&quot;month&quot;],
[type=&quot;week&quot;],
[type=&quot;time&quot;],
[type=&quot;color&quot;]
) {
height: 44px;
min-height: 44px;
font-size: max(16px, calc(var(--font-size-base) * 14 / 16));
font-size: max(
16px,
round(nearest, calc(var(--font-size-base) * 14 / 16), 1px)
);
}
.form-control[type=&quot;file&quot;]::file-selector-button {
min-width: 44px;
min-height: 44px;
}
.form-control-color[type=&quot;color&quot;] {
width: 44px;
height: 44px;
}
.form-control:is(
[type=&quot;date&quot;],
[type=&quot;datetime-local&quot;],
[type=&quot;month&quot;],
[type=&quot;time&quot;],
[type=&quot;week&quot;]
) {
min-width: 0;
max-width: 100%;
padding-block: 8px;
}
.form-check {
min-height: 44px;
padding-inline: 15px;
}
.form-check-input {
position: relative;
}
.form-check-input::after {
position: absolute;
top: 50%;
left: 50%;
width: 44px;
height: 44px;
content: &quot;&quot;;
transform: translate(-50%, -50%);
}
.form-check-label {
display: inline-flex;
min-width: 44px;
min-height: 44px;
align-items: center;
}
.form-range {
height: 44px;
touch-action: pan-y;
}
}
svg {
display: block;
max-width: 100%;
height: auto;
}
#widget &gt; svg {
width: 100%;
}
/* Model-authored chart rules can otherwise stretch icons after these styles load. */
svg.lucide {
display: block;
width: var(--icon-size) !important;
height: var(--icon-size) !important;
flex: none;
margin: 0 !important;
stroke-width: 1.6;
}
.text-warning,
.text-destructive {
color: var(--destructive);
}
html&gt;body{padding:0}&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;script type=&quot;application/json&quot; id=&quot;codex-visualization-widget-state&quot;&gt;__CODEX_VISUALIZATION_WIDGET_STATE__&lt;/script&gt;
&lt;script&gt;(() =&gt; {
const root = document.documentElement;
const mediaQuery = globalThis.matchMedia(&quot;(prefers-color-scheme: dark)&quot;);
const apply = Reflect.apply;
const isArray = Array.isArray;
const hasOwn = Object.hasOwn;
const parentWindow = globalThis.parent;
const stringify = JSON.stringify;
const parse = JSON.parse;
const encode = TextEncoder.prototype.encode.bind(new TextEncoder());
const initialStateElement = document.getElementById(
&quot;codex-visualization-widget-state&quot;,
);
const initialGlobals = parse(initialStateElement?.textContent ?? &quot;{}&quot;);
initialStateElement?.remove();
/** @type {Record&lt;string, unknown&gt; | null} */
let state = initialGlobals.widgetState ?? null;
const statePersistence = initialGlobals.statePersistence ?? &quot;none&quot;;
let nextStateRequestId = 0;
/** @type {Map&lt;number, { resolve: () =&gt; void; reject: (error: Error) =&gt; void; timeout: ReturnType&lt;typeof globalThis.setTimeout&gt;; serializedState: string }&gt;} */
const pendingStateWrites = new Map();
const persistenceError = () =&gt;
Object.assign(
new Error(&quot;The host could not persist the widget state update&quot;),
{
code: -32000,
data: { code: &quot;widget_state_persistence_failed&quot;, retryable: false },
},
);
/**
* @param {object} prototype
* @param {string} property
*/
const getPrototypeGetter = (prototype, property) =&gt; {
// oxlint-disable-next-line typescript/unbound-method -- invoked through captured Reflect.apply
const getter = Object.getOwnPropertyDescriptor(prototype, property)?.get;
if (getter == null) {
throw new Error(`Missing ${property} getter`);
}
return getter;
};
const getMessageData = getPrototypeGetter(MessageEvent.prototype, &quot;data&quot;);
const getMessagePorts = getPrototypeGetter(MessageEvent.prototype, &quot;ports&quot;);
const getMessageSource = getPrototypeGetter(MessageEvent.prototype, &quot;source&quot;);
// oxlint-disable-next-line typescript/unbound-method -- captured before the untrusted fragment runs
const addHostMessageListener = MessagePort.prototype.addEventListener;
// oxlint-disable-next-line typescript/unbound-method -- captured before the untrusted fragment runs
const postHostMessage = MessagePort.prototype.postMessage;
// oxlint-disable-next-line typescript/unbound-method -- captured before the untrusted fragment runs
const startHostPort = MessagePort.prototype.start;
// oxlint-disable-next-line typescript/unbound-method -- captured before the untrusted fragment runs
const stopMessagePropagation = Event.prototype.stopImmediatePropagation;
/** @type {MessagePort | null} */
let hostPort = null;
const postToHost = (type, payload) =&gt; {
if (hostPort != null) {
apply(postHostMessage, hostPort, [{ type, ...payload }]);
}
};
const openExternal = ({ href }) =&gt; {
if (globalThis.navigator.userActivation?.isActive === true) {
postToHost(&quot;open-external&quot;, { href });
}
};
const sendFollowUpMessage = ({ context, prompt, title }) =&gt; {
if (globalThis.navigator.userActivation?.isActive === true) {
postToHost(&quot;follow-up&quot;, { context, prompt, title });
}
return Promise.resolve();
};
/**
* @param {Record&lt;string, unknown&gt; | ((previous: Record&lt;string, unknown&gt; | null) =&gt; Record&lt;string, unknown&gt;)} next
* @returns {Promise&lt;void&gt;}
*/
const setWidgetState = async (next) =&gt; {
const nextState = typeof next === &quot;function&quot; ? next(state) : next;
if (
nextState == null ||
typeof nextState !== &quot;object&quot; ||
isArray(nextState)
) {
throw new TypeError(&quot;Widget state must be a JSON object&quot;);
}
const inputState = stringify(
nextState,
(_key, /** @type {unknown} */ value) =&gt; {
if (
typeof value === &quot;undefined&quot; ||
typeof value === &quot;function&quot; ||
typeof value === &quot;symbol&quot; ||
(typeof value === &quot;number&quot; &amp;&amp; !Number.isFinite(value))
) {
throw new TypeError(&quot;Widget state must be a JSON object&quot;);
}
return value;
},
);
const parsedState = parse(inputState);
if (
parsedState == null ||
typeof parsedState !== &quot;object&quot; ||
isArray(parsedState)
) {
throw new TypeError(&quot;Widget state must be a JSON object&quot;);
}
const storedState = {
modelContent: null,
privateContent: null,
...parsedState,
};
const serializedState = stringify(storedState);
if (encode(serializedState).length &gt; 16 * 1024) {
throw new TypeError(
&quot;Widget state must be a JSON object of at most 16 KiB&quot;,
);
}
if (
hasOwn(storedState, &quot;imageIds&quot;) &amp;&amp;
(!isArray(storedState.imageIds) ||
storedState.imageIds.some(
(/** @type {unknown} */ id) =&gt; typeof id !== &quot;string&quot;,
))
) {
throw new TypeError(&quot;Widget state imageIds must be an array of strings&quot;);
}
if (storedState.imageIds?.length &gt; 0) {
throw Object.assign(
new TypeError(&quot;Visualization state does not support image attachments&quot;),
{ code: -32602, data: { code: &quot;images_unsupported&quot; } },
);
}
state = storedState;
globalThis.openai.widgetState = state;
globalThis.dispatchEvent(
new CustomEvent(&quot;openai:set_globals&quot;, {
detail: { globals: { widgetState: state } },
}),
);
if (statePersistence === &quot;none&quot;) {
throw persistenceError();
}
const id = ++nextStateRequestId;
return new Promise((resolve, reject) =&gt; {
const timeout = globalThis.setTimeout(() =&gt; {
pendingStateWrites.delete(id);
reject(persistenceError());
}, 30_000);
pendingStateWrites.set(id, { resolve, reject, timeout, serializedState });
postToHost(&quot;widget-state-write&quot;, { id, state: serializedState });
});
};
const syncTheme = () =&gt; {
const theme = mediaQuery.matches ? &quot;dark&quot; : &quot;light&quot;;
root.dataset.theme = theme;
globalThis.openai = {
...globalThis.openai,
openExternal,
sendFollowUpMessage,
setWidgetState,
theme,
visualizationStyleVariables: {},
visualizationTheme: theme,
widgetState: state,
statePersistence,
stateModelContext: &quot;none&quot;,
};
globalThis.dispatchEvent(
new CustomEvent(&quot;openai:set_globals&quot;, {
detail: { globals: globalThis.openai },
}),
);
};
const sendHeight = () =&gt; {
postToHost(&quot;height&quot;, {
height: Math.ceil(
Math.max(
document.body.scrollHeight,
document.body.getBoundingClientRect().height,
),
),
});
};
globalThis.addEventListener(
&quot;message&quot;,
(event) =&gt; {
const data = apply(getMessageData, event, []);
const ports = apply(getMessagePorts, event, []);
const source = apply(getMessageSource, event, []);
if (
hostPort != null ||
source !== parentWindow ||
typeof data !== &quot;object&quot; ||
data?.type !== &quot;codex-visualization-initialize&quot; ||
!isArray(ports) ||
ports.length !== 1
) {
return;
}
apply(stopMessagePropagation, event, []);
const nextHostPort = ports[0];
try {
apply(addHostMessageListener, nextHostPort, [
&quot;message&quot;,
(event) =&gt; {
const data = apply(getMessageData, event, []);
if (typeof data === &quot;object&quot; &amp;&amp; data?.type === &quot;measure&quot;) {
sendHeight();
} else if (
typeof data === &quot;object&quot; &amp;&amp;
data?.type === &quot;widget-state-result&quot;
) {
/** @type {unknown} */
const id = data.id;
if (typeof id !== &quot;number&quot;) {
return;
}
const pending = pendingStateWrites.get(id);
if (pending != null) {
pendingStateWrites.delete(id);
globalThis.clearTimeout(pending.timeout);
if (data.ok === true) {
pending.resolve();
} else {
pending.reject(persistenceError());
}
}
}
},
]);
apply(startHostPort, nextHostPort, []);
} catch {
return;
}
hostPort = nextHostPort;
for (const [id, { serializedState }] of pendingStateWrites) {
postToHost(&quot;widget-state-write&quot;, { id, state: serializedState });
}
sendHeight();
},
{ capture: true },
);
document.currentScript?.remove();
mediaQuery.addEventListener(&quot;change&quot;, syncTheme);
globalThis.addEventListener(&quot;click&quot;, (event) =&gt; {
if (event.defaultPrevented) {
return;
}
let element = null;
if (event.target instanceof globalThis.Element) {
element = event.target;
} else if (event.target instanceof globalThis.Node) {
element = event.target.parentElement;
}
const link = element?.closest(&quot;a[href]&quot;);
const href = link?.getAttribute(&quot;href&quot;);
if (href == null) {
return;
}
if (href.startsWith(&quot;#&quot;)) {
let id;
try {
id = decodeURIComponent(href.slice(1));
} catch {
return;
}
const target = id.length === 0 ? root : document.getElementById(id);
if (
target == null ||
globalThis.navigator.userActivation?.isActive !== true
) {
return;
}
event.preventDefault();
postToHost(&quot;scroll-to&quot;, {
top: target.getBoundingClientRect().top + globalThis.scrollY,
});
return;
}
event.preventDefault();
openExternal({ href });
});
new ResizeObserver(sendHeight).observe(document.body);
syncTheme();
})();
&lt;/script&gt;
&lt;div id=&quot;msix-builder-concept&quot;&gt;
&lt;style&gt;
#msix-builder-concept { font-family: &quot;Segoe UI&quot;, &quot;Malgun Gothic&quot;, sans-serif; color-scheme: light dark; }
#msix-builder-concept * { box-sizing: border-box; }
#msix-builder-concept [hidden] { display: none !important; }
#msix-builder-concept .ms-app { --ms-bg: light-dark(#f6f7fa,#171b23); --ms-paper: light-dark(#ffffff,#202630); --ms-text: light-dark(#202a39,#edf1f8); --ms-muted: light-dark(#59677b,#aebacf); --ms-line: light-dark(#dce2eb,#3a4353); --ms-accent: light-dark(#245bd7,#96b7ff); --ms-soft: light-dark(#edf3ff,#273752); --ms-button: light-dark(#245bd7,#a9c3ff); --ms-onbutton: light-dark(#ffffff,#152445); --ms-good: light-dark(#17654c,#8bd7b6); --ms-warn: light-dark(#805011,#edc27e); --ms-error: light-dark(#a52c33,#ffabb0); color:var(--ms-text); background:var(--ms-bg); border:1px solid var(--ms-line); border-radius:12px; overflow:hidden; min-height:940px; display:flex; flex-direction:column; font-size:14px; line-height:1.55; }
#msix-builder-concept button, #msix-builder-concept input, #msix-builder-concept select { font:inherit; }
#msix-builder-concept button { color:inherit; cursor:inherit; }
#msix-builder-concept .ms-top { display:flex; align-items:center; gap:12px; padding:16px 22px; padding-right:104px; border-bottom:1px solid var(--ms-line); background:var(--ms-paper); flex-wrap:wrap; }
#msix-builder-concept .ms-brand { font-size:16px; font-weight:600; display:flex; gap:10px; align-items:center; }
#msix-builder-concept .ms-mark { display:grid; place-items:center; width:32px; height:32px; border-radius:9px; background:var(--ms-button); color:var(--ms-onbutton); }
#msix-builder-concept .ms-pill { padding:3px 9px; border-radius:5px; background:var(--ms-soft); color:var(--ms-accent); font-size:12px; }
#msix-builder-concept .ms-caption { color:var(--ms-muted); font-size:12px; }
#msix-builder-concept .ms-spacer { flex:1; }
#msix-builder-concept .ms-layout { flex:1; display:grid; grid-template-columns:190px minmax(0,1fr); }
#msix-builder-concept .ms-nav { padding:23px 12px; border-right:1px solid var(--ms-line); }
#msix-builder-concept .ms-project { padding:0 12px 23px; overflow-wrap:anywhere; }
#msix-builder-concept .ms-project strong { display:block; font-weight:600; margin:4px 0; }
#msix-builder-concept .ms-nav button { display:flex; align-items:center; gap:11px; width:100%; border:0; background:transparent; padding:11px 12px; text-align:left; border-radius:7px; margin-bottom:5px; }
#msix-builder-concept .ms-nav button[aria-current=&quot;step&quot;] { background:var(--ms-soft); color:var(--ms-accent); font-weight:600; }
#msix-builder-concept .ms-nav svg { flex:none; }
#msix-builder-concept .ms-nav .ms-nav-end { margin:30px 12px 0; padding-top:15px; border-top:1px solid var(--ms-line); }
#msix-builder-concept .ms-content { min-width:0; padding:27px 28px 20px; }
#msix-builder-concept h2 { margin:5px 0 7px; font-size:25px; line-height:1.35; font-weight:600; letter-spacing:-.6px; }
#msix-builder-concept h3 { margin:0 0 17px; font-size:15px; font-weight:600; }
#msix-builder-concept p { margin:0; }
#msix-builder-concept .ms-eyebrow { font-size:11px; letter-spacing:1.4px; font-weight:600; color:var(--ms-accent); }
#msix-builder-concept .ms-lead { color:var(--ms-muted); margin-bottom:25px; }
#msix-builder-concept .ms-cols { display:grid; grid-template-columns:minmax(0,1.65fr) minmax(190px,1fr); gap:20px; align-items:start; }
#msix-builder-concept .ms-panel { background:var(--ms-paper); border:1px solid var(--ms-line); border-radius:10px; padding:20px; min-width:0; }
#msix-builder-concept .ms-fields { display:grid; grid-template-columns:1fr 1fr; gap:17px; }
#msix-builder-concept .ms-full { grid-column:1/-1; }
#msix-builder-concept label.ms-field { display:flex; flex-direction:column; gap:6px; font-size:12px; font-weight:600; }
#msix-builder-concept input:not([type=&quot;checkbox&quot;]), #msix-builder-concept select { width:100%; min-width:0; padding:9px 10px; border:1px solid var(--ms-line); border-radius:6px; color:var(--ms-text); background:var(--ms-paper); font-weight:400; }
#msix-builder-concept input:focus-visible, #msix-builder-concept select:focus-visible, #msix-builder-concept button:focus-visible { outline:2px solid var(--ms-accent); outline-offset:3px; }
#msix-builder-concept .ms-field small { color:var(--ms-muted); font-size:11px; font-weight:400; }
#msix-builder-concept .ms-line { height:1px; background:var(--ms-line); margin:20px 0; }
#msix-builder-concept .ms-note { margin-top:16px; padding:12px 14px; background:var(--ms-soft); border-radius:6px; color:var(--ms-muted); font-size:12px; }
#msix-builder-concept .ms-appicon { display:grid; place-items:center; width:56px; height:56px; border-radius:13px; background:var(--ms-button); color:var(--ms-onbutton); margin-bottom:17px; }
#msix-builder-concept .ms-appicon svg { width:27px; height:27px; }
#msix-builder-concept .ms-preview-name { font-size:19px; font-weight:600; overflow-wrap:anywhere; margin-bottom:4px; }
#msix-builder-concept .ms-preview-description { font-size:12px; color:var(--ms-muted); min-height:38px; overflow-wrap:anywhere; }
#msix-builder-concept .ms-kv { display:flex; align-items:flex-start; gap:14px; justify-content:space-between; font-size:12px; padding:8px 0; }
#msix-builder-concept .ms-kv span:first-child { color:var(--ms-muted); flex-shrink:0; }
#msix-builder-concept .ms-kv span:last-child { overflow-wrap:anywhere; text-align:right; min-width:0; }
#msix-builder-concept .ms-button { display:inline-flex; justify-content:center; align-items:center; gap:7px; background:var(--ms-paper); border:1px solid var(--ms-line); border-radius:6px; padding:9px 14px; font-weight:500; min-height:39px; }
#msix-builder-concept .ms-button.primary { background:var(--ms-button); border-color:transparent; color:var(--ms-onbutton); }
#msix-builder-concept .ms-button:disabled { opacity:.5; cursor:not-allowed; }
#msix-builder-concept .ms-button.full { width:100%; margin-top:14px; }
#msix-builder-concept .ms-footer { display:flex; gap:12px; justify-content:space-between; align-items:center; margin-top:24px; flex-wrap:wrap; }
#msix-builder-concept .ms-actions { display:flex; gap:8px; flex-wrap:wrap; }
#msix-builder-concept .ms-status { font-size:12px; color:var(--ms-muted); }
#msix-builder-concept .ms-check { display:flex; gap:10px; align-items:flex-start; margin:13px 0; }
#msix-builder-concept input[type=&quot;checkbox&quot;] { width:17px; height:17px; accent-color:var(--ms-accent); margin:3px 0 0; flex:none; }
#msix-builder-concept .ms-check small { display:block; font-size:12px; color:var(--ms-muted); margin-top:3px; }
#msix-builder-concept .ms-toolbar { display:flex; justify-content:space-between; gap:10px; align-items:center; margin-bottom:16px; flex-wrap:wrap; }
#msix-builder-concept .ms-toolbar h3 { margin:0; }
#msix-builder-concept .ms-file { display:flex; align-items:center; gap:11px; padding:13px 0; border-bottom:1px solid var(--ms-line); min-width:0; }
#msix-builder-concept .ms-file:last-child { border-bottom:0; }
#msix-builder-concept .ms-file .ms-filename { flex:1; min-width:0; overflow-wrap:anywhere; }
#msix-builder-concept .ms-file small { display:block; color:var(--ms-muted); font-size:11px; }
#msix-builder-concept .ms-file &gt; span:last-child { font-size:12px; color:var(--ms-muted); white-space:nowrap; }
#msix-builder-concept .ms-file svg { color:var(--ms-muted); flex:none; }
#msix-builder-concept .ms-choice { display:flex; align-items:center; gap:10px; padding:14px; border:1px solid var(--ms-line); border-radius:7px; margin-bottom:10px; }
#msix-builder-concept .ms-choice.active { border-color:var(--ms-accent); background:var(--ms-soft); }
#msix-builder-concept .ms-choice strong { display:block; font-size:13px; font-weight:600; }
#msix-builder-concept .ms-choice small { color:var(--ms-muted); font-size:12px; }
#msix-builder-concept .ms-good { color:var(--ms-good); }
#msix-builder-concept .ms-warn { color:var(--ms-warn); }
#msix-builder-concept .ms-error { color:var(--ms-error); }
#msix-builder-concept .ms-alert { padding:13px 15px; border:1px solid var(--ms-line); border-left:3px solid var(--ms-error); background:var(--ms-paper); border-radius:6px; margin-bottom:17px; font-size:12px; }
#msix-builder-concept .ms-alert button { margin-top:10px; }
#msix-builder-concept .ms-stage { display:flex; align-items:flex-start; gap:12px; padding:14px 0; border-bottom:1px solid var(--ms-line); }
#msix-builder-concept .ms-stage:last-child { border:0; }
#msix-builder-concept .ms-stage .ms-stage-title { flex:1; min-width:0; }
#msix-builder-concept .ms-stage small { display:block; color:var(--ms-muted); font-size:12px; }
#msix-builder-concept .ms-stage &gt; span:last-child { font-size:12px; }
#msix-builder-concept .ms-meter { height:6px; border-radius:4px; background:var(--ms-line); overflow:hidden; margin:12px 0; }
#msix-builder-concept .ms-meter div { height:100%; background:var(--ms-accent); }
#msix-builder-concept .ms-code { font:12px/1.8 Consolas,monospace; white-space:pre-wrap; overflow-wrap:anywhere; color:var(--ms-muted); margin:0; }
#msix-builder-concept .ms-bottom { display:flex; gap:10px; justify-content:space-between; padding:11px 22px; border-top:1px solid var(--ms-line); background:var(--ms-paper); flex-wrap:wrap; font-size:11px; color:var(--ms-muted); }
#msix-builder-concept .ms-steps { display:grid; grid-template-columns:repeat(6,1fr); gap:8px; padding:22px 26px 0; }
#msix-builder-concept .ms-steps button { border:0; background:transparent; padding:8px 0; display:flex; flex-direction:column; align-items:center; gap:7px; color:var(--ms-muted); font-size:12px; }
#msix-builder-concept .ms-step-dot { width:27px; height:27px; border:1px solid var(--ms-line); border-radius:50%; display:grid; place-items:center; background:var(--ms-paper); font-size:12px; }
#msix-builder-concept .ms-steps button[aria-current=&quot;step&quot;] { color:var(--ms-accent); }
#msix-builder-concept .ms-steps button[aria-current=&quot;step&quot;] .ms-step-dot { background:var(--ms-button); color:var(--ms-onbutton); border-color:transparent; }
#msix-builder-concept .ms-guided .ms-content { flex:1; padding:24px 40px; }
#msix-builder-concept .ms-guided .ms-cols { grid-template-columns:minmax(0,1.6fr) minmax(210px,1fr); }
#msix-builder-concept .ms-guided { --ms-accent:light-dark(#146d60,#82d7c6); --ms-soft:light-dark(#eaf5f2,#263e3a); --ms-button:light-dark(#146d60,#9ae1d2); --ms-onbutton:light-dark(#ffffff,#14332e); }
@media(max-width:800px) { #msix-builder-concept .ms-layout { grid-template-columns:154px minmax(0,1fr); } #msix-builder-concept .ms-content { padding:22px 18px; } #msix-builder-concept .ms-cols, #msix-builder-concept .ms-guided .ms-cols { grid-template-columns:1fr; } #msix-builder-concept .ms-side { display:grid; grid-template-columns:1fr 1fr; gap:16px; } #msix-builder-concept .ms-side &gt; .ms-caption { grid-column:1/-1; } #msix-builder-concept .ms-guided .ms-content { padding:22px; } }
@media(max-width:520px) { #msix-builder-concept .ms-app { min-height:1900px; } #msix-builder-concept .ms-layout { grid-template-columns:1fr; } #msix-builder-concept .ms-nav { border-right:0; border-bottom:1px solid var(--ms-line); padding:12px; display:grid; grid-template-columns:1fr 1fr; gap:3px; } #msix-builder-concept .ms-project, #msix-builder-concept .ms-nav-end { display:none; } #msix-builder-concept .ms-nav button { padding:8px; margin:0; font-size:12px; } #msix-builder-concept .ms-fields, #msix-builder-concept .ms-side { grid-template-columns:1fr; } #msix-builder-concept .ms-top { padding:14px 16px; padding-right:96px; } #msix-builder-concept .ms-top .ms-caption { display:none; } #msix-builder-concept .ms-content, #msix-builder-concept .ms-guided .ms-content { padding:20px 14px; } #msix-builder-concept .ms-panel { padding:16px; } #msix-builder-concept .ms-steps { padding:12px; grid-template-columns:repeat(3,1fr); } #msix-builder-concept .ms-footer { align-items:stretch; } #msix-builder-concept .ms-actions { flex:1; justify-content:flex-end; } #msix-builder-concept .ms-bottom { padding:10px 14px; } }
@media(pointer:coarse) { #msix-builder-concept .ms-button, #msix-builder-concept .ms-nav button, #msix-builder-concept .ms-steps button { min-height:44px; } #msix-builder-concept input:not([type=&quot;checkbox&quot;]), #msix-builder-concept select { font-size:16px; } }
&lt;/style&gt;
&lt;div class=&quot;viz-carousel&quot; aria-label=&quot;MSIX 제작기 디자인&quot; data-previous-label=&quot;이전 시안&quot; data-next-label=&quot;다음 시안&quot;&gt;
&lt;section data-variant=&quot;프로젝트 작업실&quot; aria-label=&quot;프로젝트 작업실&quot;&gt;
&lt;div class=&quot;ms-app&quot; data-app=&quot;studio&quot;&gt;&lt;/div&gt;
&lt;/section&gt;
&lt;section data-variant=&quot;단계별 제작&quot; aria-label=&quot;단계별 제작&quot; hidden&gt;
&lt;div class=&quot;ms-app ms-guided&quot; data-app=&quot;guided&quot;&gt;&lt;/div&gt;
&lt;/section&gt;
&lt;/div&gt;
&lt;script&gt;
(() =&gt; {
const root = document.getElementById(&#x27;msix-builder-concept&#x27;);
const pages = [[&#x27;app&#x27;,&#x27;앱 정보&#x27;,&#x27;panels-top-left&#x27;],[&#x27;files&#x27;,&#x27;파일 구성&#x27;,&#x27;folder-open&#x27;],[&#x27;manifest&#x27;,&#x27;패키지 설정&#x27;,&#x27;package&#x27;],[&#x27;sign&#x27;,&#x27;서명&#x27;,&#x27;shield-check&#x27;],[&#x27;update&#x27;,&#x27;업데이트&#x27;,&#x27;refresh-cw&#x27;],[&#x27;build&#x27;,&#x27;검증 · 빌드&#x27;,&#x27;hammer&#x27;]];
const defaults = () =&gt; ({page:&#x27;app&#x27;,name:&#x27;Orbit Notes&#x27;,description:&#x27;팀의 아이디어를 한곳에&#x27;,publisherName:&#x27;Example Studio&#x27;,identity:&#x27;ExampleStudio.OrbitNotes&#x27;,publisher:&#x27;CN=Example Studio&#x27;,version:&#x27;1.2.0.0&#x27;,arch:&#x27;x64&#x27;,exe:&#x27;OrbitNotes.exe&#x27;,minOs:&#x27;10.0.19041.0&#x27;,sign:&#x27;development&#x27;,certSubject:&#x27;CN=Example Studio&#x27;,autoUpdate:false,url:&#x27;https://downloads.example.com/orbit/stable/OrbitNotes.appinstaller&#x27;,interval:&#x27;24&#x27;,prompt:true,extraFile:false,revision:1,validated:false,build:&#x27;idle&#x27;,log:false,message:&#x27;&#x27;,error:&#x27;&#x27;});
const state = {studio:defaults(),guided:defaults()};
const sessionId = Date.now().toString(36) + Math.random().toString(36).slice(2);
const esc = v =&gt; String(v).replace(/[&amp;&lt;&gt;&quot;&#x27;]/g,c=&gt;({&#x27;&amp;&#x27;:&#x27;&amp;amp;&#x27;,&#x27;&lt;&#x27;:&#x27;&amp;lt;&#x27;,&#x27;&gt;&#x27;:&#x27;&amp;gt;&#x27;,&#x27;&quot;&#x27;:&#x27;&amp;quot;&#x27;,&quot;&#x27;&quot;:&#x27;&amp;#39;&#x27;}[c]));
const icon = name =&gt; `&lt;i data-lucide=&quot;${name}&quot; aria-hidden=&quot;true&quot;&gt;&lt;/i&gt;`;
const field = (s,key,label,full=false,hint=&#x27;&#x27;) =&gt; `&lt;label class=&quot;ms-field ${full?&#x27;ms-full&#x27;:&#x27;&#x27;}&quot;&gt;${label}&lt;input data-field=&quot;${key}&quot; value=&quot;${esc(s[key])}&quot; ${key===&#x27;url&#x27;?&#x27;type=&quot;url&quot;&#x27;:&#x27;type=&quot;text&quot;&#x27;}&gt;${hint?`&lt;small&gt;${hint}&lt;/small&gt;`:&#x27;&#x27;}&lt;/label&gt;`;
const select = (s,key,label,opts) =&gt; `&lt;label class=&quot;ms-field&quot;&gt;${label}&lt;select data-field=&quot;${key}&quot;&gt;${opts.map(([v,l])=&gt;`&lt;option value=&quot;${v}&quot; ${s[key]===v?&#x27;selected&#x27;:&#x27;&#x27;}&gt;${l}&lt;/option&gt;`).join(&#x27;&#x27;)}&lt;/select&gt;&lt;/label&gt;`;
const check = (s,key,label,hint=&#x27;&#x27;) =&gt; `&lt;label class=&quot;ms-check&quot;&gt;&lt;input type=&quot;checkbox&quot; data-field=&quot;${key}&quot; ${s[key]?&#x27;checked&#x27;:&#x27;&#x27;}&gt;&lt;span&gt;${label}${hint?`&lt;small&gt;${hint}&lt;/small&gt;`:&#x27;&#x27;}&lt;/span&gt;&lt;/label&gt;`;
const kv=(a,b)=&gt;`&lt;div class=&quot;ms-kv&quot;&gt;&lt;span&gt;${a}&lt;/span&gt;&lt;span&gt;${b}&lt;/span&gt;&lt;/div&gt;`;
function validate(s) {
if(!s.name.trim()||!s.identity.trim()||!s.publisher.trim()) return [&#x27;app&#x27;,&#x27;제품 이름과 패키지 식별 정보를 입력해 주세요.&#x27;];
if(!/^\d+\.\d+\.\d+\.\d+$/.test(s.version)||s.version.split(&#x27;.&#x27;).some(n=&gt;Number(n)&gt;65535)||Number(s.version.split(&#x27;.&#x27;)[0])===0) return [&#x27;app&#x27;,&#x27;버전을 1.2.0.0처럼 네 부분 숫자로 입력해 주세요.&#x27;];
if(![&#x27;OrbitNotes.exe&#x27;].includes(s.exe)) return [&#x27;files&#x27;,&#x27;실행 파일이 패키지 목록에 없습니다. OrbitNotes.exe를 지정해 주세요.&#x27;];
if(s.sign!==&#x27;none&#x27; &amp;&amp; s.publisher!==s.certSubject) return [&#x27;sign&#x27;,&#x27;Publisher와 인증서 Subject가 일치하지 않습니다.&#x27;];
if(s.autoUpdate) { try { const u=new URL(s.url); if(u.protocol!==&#x27;https:&#x27;||!u.pathname.endsWith(&#x27;.appinstaller&#x27;)) throw Error(); } catch { return [&#x27;update&#x27;,&#x27;HTTPS 주소와 .appinstaller 파일 이름을 확인해 주세요.&#x27;]; } }
return null;
}
function restore(saved) {
if(!saved||saved.schema!==1)return;
[&#x27;studio&#x27;,&#x27;guided&#x27;].forEach(k=&gt;{
const d=defaults(), v=saved[k];if(!v||typeof v!==&#x27;object&#x27;)return;
Object.keys(d).forEach(f=&gt;{if(typeof v[f]===typeof d[f]&amp;&amp;(![&#x27;string&#x27;].includes(typeof v[f])||v[f].length&lt;1000))d[f]=v[f];});
if(!pages.some(p=&gt;p[0]===d.page))d.page=&#x27;app&#x27;;
if(![&#x27;development&#x27;,&#x27;production&#x27;,&#x27;none&#x27;].includes(d.sign))d.sign=&#x27;development&#x27;;
d.validated=false;d.build=&#x27;idle&#x27;;d.error=&#x27;&#x27;;d.message=&#x27;&#x27;;state[k]=d;
});
}
restore(window.openai?.widgetState?.privateContent);
function persist(){ const host=window.openai;if(host?.setWidgetState)host.setWidgetState({modelContent:{design:&#x27;MSIX-first builder&#x27;,studioPage:state.studio.page,guidedPage:state.guided.page},privateContent:{schema:1,sessionId,...state}}).catch(()=&gt;{}); }
function preview(s) {
return `&lt;aside class=&quot;ms-side&quot;&gt;&lt;div class=&quot;ms-panel&quot;&gt;&lt;h3&gt;앱 표시 미리보기&lt;/h3&gt;&lt;div class=&quot;ms-appicon&quot;&gt;${icon(&#x27;notebook-pen&#x27;)}&lt;/div&gt;&lt;div class=&quot;ms-preview-name&quot;&gt;${esc(s.name||&#x27;앱 이름&#x27;)}&lt;/div&gt;&lt;p class=&quot;ms-preview-description&quot;&gt;${esc(s.description)}&lt;/p&gt;&lt;div class=&quot;ms-line&quot;&gt;&lt;/div&gt;${kv(&#x27;게시자&#x27;,esc(s.publisherName))}${kv(&#x27;버전&#x27;,esc(s.version))}${kv(&#x27;아키텍처&#x27;,esc(s.arch))}${kv(&#x27;설치 위치&#x27;,&#x27;Windows에서 관리&#x27;)}&lt;div class=&quot;ms-note&quot;&gt;설치 폴더 선택 없이 패키지로 배포합니다.&lt;/div&gt;&lt;/div&gt;&lt;p class=&quot;ms-caption&quot; style=&quot;margin-top:12px&quot;&gt;표시 정보 예시입니다. 실제 App Installer 화면은 Windows 환경에 따라 달라집니다.&lt;/p&gt;&lt;/aside&gt;`;
}
function side(s,title,body){return `&lt;aside class=&quot;ms-side&quot;&gt;&lt;div class=&quot;ms-panel&quot;&gt;&lt;h3&gt;${title}&lt;/h3&gt;${body}&lt;/div&gt;&lt;/aside&gt;`;}
function pane(s) {
if(s.page===&#x27;app&#x27;)return `&lt;div class=&quot;ms-cols&quot;&gt;&lt;div class=&quot;ms-panel&quot;&gt;&lt;h3&gt;사용자에게 표시할 정보&lt;/h3&gt;&lt;div class=&quot;ms-fields&quot;&gt;${field(s,&#x27;name&#x27;,&#x27;앱 이름&#x27;,true)}${field(s,&#x27;description&#x27;,&#x27;설명&#x27;,true)}${field(s,&#x27;publisherName&#x27;,&#x27;게시자 표시명&#x27;,true)}${field(s,&#x27;version&#x27;,&#x27;패키지 버전&#x27;,false,&#x27;네 부분 숫자 버전&#x27;)}${select(s,&#x27;arch&#x27;,&#x27;아키텍처&#x27;,[[&#x27;x64&#x27;,&#x27;x64&#x27;],[&#x27;arm64&#x27;,&#x27;ARM64&#x27;]])}&lt;/div&gt;&lt;div class=&quot;ms-line&quot;&gt;&lt;/div&gt;&lt;div class=&quot;ms-toolbar&quot;&gt;&lt;span&gt;앱 아이콘&lt;/span&gt;&lt;span class=&quot;ms-caption&quot;&gt;Assets / Logo.png · 예시&lt;/span&gt;&lt;/div&gt;&lt;p class=&quot;ms-caption&quot;&gt;실제 제품에서는 로고 파일과 크기를 검사합니다.&lt;/p&gt;&lt;/div&gt;${preview(s)}&lt;/div&gt;`;
if(s.page===&#x27;files&#x27;)return `&lt;div class=&quot;ms-cols&quot;&gt;&lt;div class=&quot;ms-panel&quot;&gt;&lt;div class=&quot;ms-toolbar&quot;&gt;&lt;h3&gt;패키지에 포함할 파일&lt;/h3&gt;&lt;button class=&quot;ms-button cursor-interaction&quot; data-action=&quot;add-file&quot;&gt;${icon(s.extraFile?&#x27;minus&#x27;:&#x27;plus&#x27;)}${s.extraFile?&#x27;예제 파일 제거&#x27;:&#x27;예제 파일 추가&#x27;}&lt;/button&gt;&lt;/div&gt;&lt;p class=&quot;ms-caption&quot;&gt;소스 폴더 · D:\\releases\\orbit-notes&lt;/p&gt;${[[&#x27;OrbitNotes.exe&#x27;,&#x27;앱 실행 파일&#x27;,&#x27;18.4 MB&#x27;],[&#x27;Orbit.Core.dll&#x27;,&#x27;애플리케이션 라이브러리&#x27;,&#x27;6.2 MB&#x27;],[&#x27;Assets/Logo.png&#x27;,&#x27;앱 이미지 · 예시&#x27;,&#x27;24 KB&#x27;],[&#x27;Resources/ko-KR.json&#x27;,&#x27;한국어 리소스&#x27;,&#x27;12 KB&#x27;],...(s.extraFile?[[&#x27;Resources/en-US.json&#x27;,&#x27;추가한 예제 파일&#x27;,&#x27;10 KB&#x27;]]:[])].map(([n,d,z])=&gt;`&lt;div class=&quot;ms-file&quot;&gt;${icon(n.endsWith(&#x27;exe&#x27;)?&#x27;file-code&#x27;:&#x27;file&#x27;)}&lt;div class=&quot;ms-filename&quot;&gt;${esc(n)}&lt;small&gt;${d}&lt;/small&gt;&lt;/div&gt;&lt;span&gt;${z}&lt;/span&gt;&lt;/div&gt;`).join(&#x27;&#x27;)}&lt;div class=&quot;ms-line&quot;&gt;&lt;/div&gt;${field(s,&#x27;exe&#x27;,&#x27;실행 파일&#x27;,true,&#x27;패키지 내부 상대 경로&#x27;)}&lt;/div&gt;${side(s,&#x27;파일 구성 원칙&#x27;,`${kv(&#x27;포함 파일&#x27;,`${s.extraFile?5:4}개 · 예시`)}${kv(&#x27;배치 위치&#x27;,&#x27;패키지 내부&#x27;)}${kv(&#x27;원본 파일&#x27;,&#x27;변경하지 않음&#x27;)}&lt;div class=&quot;ms-note&quot;&gt;문서·설정·로그는 앱 데이터 위치를 사용합니다. 설치 폴더 쓰기에 의존하는 앱은 호환성을 확인해야 합니다.&lt;/div&gt;`)}&lt;/div&gt;`;
if(s.page===&#x27;manifest&#x27;)return `&lt;div class=&quot;ms-cols&quot;&gt;&lt;div class=&quot;ms-panel&quot;&gt;&lt;h3&gt;패키지 식별 정보&lt;/h3&gt;&lt;div class=&quot;ms-fields&quot;&gt;${field(s,&#x27;identity&#x27;,&#x27;패키지 이름 · Identity Name&#x27;,true)}${field(s,&#x27;publisher&#x27;,&#x27;Publisher&#x27;,true,&#x27;서명 인증서의 Subject와 일치해야 합니다.&#x27;)}${select(s,&#x27;minOs&#x27;,&#x27;최소 Windows 버전&#x27;,[[&#x27;10.0.19041.0&#x27;,&#x27;Windows 10 · 2004 이상&#x27;],[&#x27;10.0.22000.0&#x27;,&#x27;Windows 11 이상&#x27;]])}&lt;/div&gt;&lt;div class=&quot;ms-line&quot;&gt;&lt;/div&gt;&lt;h3&gt;앱 실행 방식&lt;/h3&gt;&lt;div class=&quot;ms-choice active&quot;&gt;${icon(&#x27;monitor&#x27;)}&lt;div&gt;&lt;strong&gt;일반 데스크톱 앱&lt;/strong&gt;&lt;small&gt;Windows.FullTrustApplication&lt;/small&gt;&lt;/div&gt;&lt;/div&gt;&lt;p class=&quot;ms-caption&quot;&gt;runFullTrust 선언을 포함합니다. 서비스·드라이버·임의 설치 작업은 초기 지원 범위에 포함하지 않습니다.&lt;/p&gt;&lt;/div&gt;${side(s,&#x27;생성할 manifest&#x27;,`&lt;pre class=&quot;ms-code&quot;&gt;&amp;lt;Identity\n Name=&quot;${esc(s.identity)}&quot;\n Publisher=&quot;${esc(s.publisher)}&quot;\n Version=&quot;${esc(s.version)}&quot;\n ProcessorArchitecture=&quot;${esc(s.arch)}&quot; /&amp;gt;&lt;/pre&gt;&lt;div class=&quot;ms-note&quot;&gt;미리보기는 요약입니다. 실제 생성 시 SDK 스키마·자산·의존성을 함께 검사합니다.&lt;/div&gt;`)}&lt;/div&gt;`;
if(s.page===&#x27;sign&#x27;)return `&lt;div class=&quot;ms-cols&quot;&gt;&lt;div class=&quot;ms-panel&quot;&gt;&lt;h3&gt;서명 프로필&lt;/h3&gt;${select(s,&#x27;sign&#x27;,&#x27;사용할 프로필&#x27;,[[&#x27;development&#x27;,&#x27;개발용 인증서 · 예시&#x27;],[&#x27;production&#x27;,&#x27;운영 서명 프로필 · 예시&#x27;],[&#x27;none&#x27;,&#x27;미서명 개발 빌드&#x27;]])}&lt;div class=&quot;ms-line&quot;&gt;&lt;/div&gt;${s.sign===&#x27;none&#x27;?&#x27;&lt;p class=&quot;ms-warn&quot;&gt;미서명 패키지는 직접 배포 준비 상태가 될 수 없습니다.&lt;/p&gt;&#x27;:`${field(s,&#x27;certSubject&#x27;,&#x27;인증서 Subject · 예시&#x27;,true)}&lt;div class=&quot;ms-line&quot;&gt;&lt;/div&gt;${kv(&#x27;패키지 Publisher&#x27;,esc(s.publisher))}${kv(&#x27;이름 일치&#x27;,s.publisher===s.certSubject?&#x27;&lt;span class=&quot;ms-good&quot;&gt;일치&lt;/span&gt;&#x27;:&#x27;&lt;span class=&quot;ms-error&quot;&gt;불일치&lt;/span&gt;&#x27;)}`}&lt;div class=&quot;ms-note&quot;&gt;개인 키나 인증서 암호를 프로젝트에 저장하지 않습니다. 실제 제품에서는 OS 또는 서명 서비스 프로필을 참조합니다.&lt;/div&gt;&lt;/div&gt;${side(s,&#x27;배포 전 확인&#x27;,`&lt;div class=&quot;ms-stage&quot;&gt;${icon(&#x27;shield-check&#x27;)}&lt;div class=&quot;ms-stage-title&quot;&gt;패키지 서명&lt;small&gt;서명자·체인·타임스탬프 검사&lt;/small&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ms-stage&quot;&gt;${icon(&#x27;monitor-check&#x27;)}&lt;div class=&quot;ms-stage-title&quot;&gt;대상 PC 신뢰&lt;small&gt;개발용 인증서 성공과 구분&lt;/small&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ms-note&quot;&gt;이 시안은 인증서 저장소에 접근하거나 서명하지 않습니다.&lt;/div&gt;`)}&lt;/div&gt;`;
if(s.page===&#x27;update&#x27;)return `&lt;div class=&quot;ms-cols&quot;&gt;&lt;div class=&quot;ms-panel&quot;&gt;&lt;h3&gt;직접 배포&lt;/h3&gt;&lt;div class=&quot;ms-choice active&quot;&gt;${icon(&#x27;globe&#x27;)}&lt;div&gt;&lt;strong&gt;내 서버에서 배포&lt;/strong&gt;&lt;small&gt;Store 계정 없이 MSIX를 전달합니다.&lt;/small&gt;&lt;/div&gt;&lt;/div&gt;${check(s,&#x27;autoUpdate&#x27;,&#x27;App Installer 업데이트 사용&#x27;,&#x27;MSIX와 함께 .appinstaller 파일을 생성합니다.&#x27;)}${s.autoUpdate?`&lt;div class=&quot;ms-line&quot;&gt;&lt;/div&gt;&lt;div class=&quot;ms-fields&quot;&gt;${field(s,&#x27;url&#x27;,&#x27;업데이트 안내 파일 주소&#x27;,true,&#x27;패키지 파일은 버전별 고정 URL을 사용합니다.&#x27;)}${select(s,&#x27;interval&#x27;,&#x27;실행 시 확인 간격&#x27;,[[&#x27;0&#x27;,&#x27;앱 실행마다&#x27;],[&#x27;24&#x27;,&#x27;24시간&#x27;],[&#x27;48&#x27;,&#x27;48시간&#x27;]])}&lt;/div&gt;${check(s,&#x27;prompt&#x27;,&#x27;업데이트 안내 표시&#x27;)}`:&#x27;&lt;div class=&quot;ms-note&quot;&gt;MSIX 파일을 직접 전달합니다. 자동 업데이트 경로는 연결되지 않습니다.&lt;/div&gt;&#x27;}&lt;/div&gt;${side(s,&#x27;생성할 결과&#x27;,`${kv(&#x27;설치 패키지&#x27;,&#x27;.msix&#x27;)}${kv(&#x27;업데이트 안내&#x27;,s.autoUpdate?&#x27;.appinstaller&#x27;:&#x27;사용 안 함&#x27;)}${kv(&#x27;업데이트 적용&#x27;,&#x27;Windows 배포 기능&#x27;)}&lt;div class=&quot;ms-note&quot;&gt;새 버전 전체 패키지를 생성합니다. Windows의 차등 업데이트를 이용하며 별도 MSP 파일을 만들지 않습니다.&lt;/div&gt;&lt;p class=&quot;ms-caption&quot; style=&quot;margin-top:14px&quot;&gt;Store 등록·자동 업로드·그룹 배포 서버는 후속 단계입니다.&lt;/p&gt;`)}&lt;/div&gt;`;
const completed=s.build===&#x27;done&#x27;,failed=s.build===&#x27;failed&#x27;,ready=s.validated;
const stages=[[&#x27;설정과 파일 검사&#x27;,&#x27;manifest · 입력 · 경로&#x27;,&#x27;list-checks&#x27;],[&#x27;MSIX 패키징&#x27;,&#x27;Windows SDK · MakeAppx&#x27;,&#x27;package&#x27;],[&#x27;서명과 무결성&#x27;,s.sign===&#x27;none&#x27;?&#x27;미서명 개발 결과&#x27;:&#x27;서명 프로필 · 해시&#x27;,&#x27;shield-check&#x27;],[&#x27;결과 보관&#x27;,&#x27;불변 아티팩트 · 검증 보고서&#x27;,&#x27;archive&#x27;]];
return `&lt;div class=&quot;ms-cols&quot;&gt;&lt;div class=&quot;ms-panel&quot;&gt;&lt;div class=&quot;ms-toolbar&quot;&gt;&lt;h3&gt;빌드 계획&lt;/h3&gt;&lt;span class=&quot;ms-pill&quot;&gt;${completed?&#x27;모의 빌드 완료&#x27;:failed?&#x27;검증 오류&#x27;:ready?&#x27;모의 검사 통과&#x27;:&#x27;검사 대기&#x27;}&lt;/span&gt;&lt;/div&gt;${stages.map(([t,d,i],n)=&gt;`&lt;div class=&quot;ms-stage&quot;&gt;${icon(completed?&#x27;circle-check&#x27;:i)}&lt;div class=&quot;ms-stage-title&quot;&gt;${t}&lt;small&gt;${d}&lt;/small&gt;&lt;/div&gt;&lt;span class=&quot;${completed?&#x27;ms-good&#x27;:&#x27;ms-caption&#x27;}&quot;&gt;${completed?(s.sign===&#x27;none&#x27;&amp;&amp;n===2?&#x27;서명 생략&#x27;:&#x27;모의 완료&#x27;):&#x27;대기&#x27;}&lt;/span&gt;&lt;/div&gt;`).join(&#x27;&#x27;)}&lt;div class=&quot;ms-meter&quot; role=&quot;progressbar&quot; aria-label=&quot;모의 빌드 진행&quot; aria-valuemin=&quot;0&quot; aria-valuemax=&quot;100&quot; aria-valuenow=&quot;${completed?100:0}&quot;&gt;&lt;div style=&quot;width:${completed?100:0}%&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class=&quot;ms-actions&quot;&gt;&lt;button class=&quot;ms-button cursor-interaction&quot; data-action=&quot;validate&quot;&gt;설정 검사&lt;/button&gt;&lt;button class=&quot;ms-button primary cursor-interaction&quot; data-action=&quot;build&quot;&gt;${icon(&#x27;play&#x27;)}모의 빌드&lt;/button&gt;&lt;/div&gt;${completed?`&lt;div class=&quot;ms-line&quot;&gt;&lt;/div&gt;&lt;h3&gt;결과 예시&lt;/h3&gt;&lt;pre class=&quot;ms-code&quot;&gt;OrbitNotes_${esc(s.version)}_${esc(s.arch)}.msix\n${s.autoUpdate?&#x27;OrbitNotes.appinstaller\n&#x27;:&#x27;&#x27;}release.json\nvalidation-report.json&lt;/pre&gt;&lt;button class=&quot;ms-button cursor-interaction&quot; data-action=&quot;log&quot; style=&quot;margin-top:14px&quot;&gt;${s.log?&#x27;기록 닫기&#x27;:&#x27;모의 기록 보기&#x27;}&lt;/button&gt;${s.log?&#x27;&lt;pre class=&quot;ms-code&quot; style=&quot;margin-top:12px&quot;&gt;[예시] 입력 스냅샷 고정\n[예시] MakeAppx 단계 완료\n[예시] 서명 정책 적용\n[예시] 결과 목록 생성\n실제 명령 실행 및 파일 생성 없음&lt;/pre&gt;&#x27;:&#x27;&#x27;}`:&#x27;&#x27;}&lt;/div&gt;${side(s,&#x27;릴리스 준비 상태&#x27;,`${kv(&#x27;패키지 생성&#x27;,completed?&#x27;모의 완료&#x27;:&#x27;대기&#x27;)}${kv(&#x27;서명&#x27;,s.sign===&#x27;none&#x27;?&#x27;생략&#x27;:completed?&#x27;모의 완료&#x27;:&#x27;대기&#x27;)}${kv(&#x27;설치·실행 시험&#x27;,&#x27;미실행&#x27;)}${kv(&#x27;업데이트·제거 시험&#x27;,&#x27;미실행&#x27;)}&lt;div class=&quot;ms-note&quot;&gt;${s.sign===&#x27;development&#x27;?&#x27;개발용 인증서로 설정되어 있습니다. 실제 사용자 배포에는 대상 PC 신뢰와 설치 시험이 필요합니다.&#x27;:&#x27;빌드와 배포 준비는 별도 상태입니다. 실제 VM 검증 증빙이 필요합니다.&#x27;}&lt;/div&gt;`)}&lt;/div&gt;`;
}
const descriptions={app:&#x27;앱 이름과 버전을 입력하면 패키지 표시 정보를 미리 확인할 수 있습니다.&#x27;,files:&#x27;배포할 파일을 모으고 앱의 실행 파일을 지정합니다.&#x27;,manifest:&#x27;Windows가 앱을 식별할 이름과 실행 조건을 설정합니다.&#x27;,sign:&#x27;패키지 게시자와 서명 프로필을 연결합니다.&#x27;,update:&#x27;MSIX만 전달하거나, 내 서버를 통한 업데이트 경로를 연결합니다.&#x27;,build:&#x27;생성 전 설정을 검사하고 패키지와 검증 결과를 확인합니다.&#x27;};
function render(k) {
const app=root.querySelector(`[data-app=&quot;${k}&quot;]`),s=state[k],idx=pages.findIndex(p=&gt;p[0]===s.page);
const nav=pages.map(([id,label,ic],n)=&gt;`&lt;button class=&quot;cursor-interaction&quot; data-page=&quot;${id}&quot; ${s.page===id?&#x27;aria-current=&quot;step&quot;&#x27;:&#x27;&#x27;}&gt;${k===&#x27;guided&#x27;?`&lt;span class=&quot;ms-step-dot&quot;&gt;${n+1}&lt;/span&gt;`:icon(ic)}&lt;span&gt;${label}&lt;/span&gt;&lt;/button&gt;`).join(&#x27;&#x27;);
const error=s.error?`&lt;div class=&quot;ms-alert&quot; role=&quot;alert&quot;&gt;${esc(s.error)}${s.errorPage?`&lt;br&gt;&lt;button class=&quot;ms-button cursor-interaction&quot; data-page=&quot;${esc(s.errorPage)}&quot;&gt;해당 설정으로 이동&lt;/button&gt;`:&#x27;&#x27;}&lt;/div&gt;`:&#x27;&#x27;;
app.innerHTML=`&lt;header class=&quot;ms-top&quot;&gt;&lt;div class=&quot;ms-brand&quot;&gt;&lt;span class=&quot;ms-mark&quot;&gt;${icon(&#x27;package&#x27;)}&lt;/span&gt;MSIX Studio&lt;/div&gt;&lt;span class=&quot;ms-pill&quot;&gt;디자인 시안&lt;/span&gt;&lt;span class=&quot;ms-spacer&quot;&gt;&lt;/span&gt;&lt;span class=&quot;ms-caption&quot;&gt;직접 배포용 패키지 제작&lt;/span&gt;&lt;/header&gt;${k===&#x27;guided&#x27;?`&lt;nav class=&quot;ms-steps&quot; aria-label=&quot;제작 단계&quot;&gt;${nav}&lt;/nav&gt;`:&#x27;&lt;div class=&quot;ms-layout&quot;&gt;&#x27;+`&lt;nav class=&quot;ms-nav&quot; aria-label=&quot;프로젝트 설정&quot;&gt;&lt;div class=&quot;ms-project&quot;&gt;&lt;span class=&quot;ms-caption&quot;&gt;현재 프로젝트&lt;/span&gt;&lt;strong&gt;${esc(s.name)}&lt;/strong&gt;&lt;span class=&quot;ms-pill&quot;&gt;MSIX · ${esc(s.arch)}&lt;/span&gt;&lt;/div&gt;${nav}&lt;div class=&quot;ms-nav-end ms-caption&quot;&gt;Windows 패키지 제작&lt;br&gt;단일 제품 · 단일 아키텍처&lt;/div&gt;&lt;/nav&gt;`}&lt;main class=&quot;ms-content&quot;&gt;&lt;div class=&quot;ms-eyebrow&quot;&gt;${k===&#x27;guided&#x27;?`STEP ${idx+1} OF 6`:&#x27;PACKAGE WORKSPACE&#x27;}&lt;/div&gt;&lt;h2&gt;${pages[idx][1]}&lt;/h2&gt;&lt;p class=&quot;ms-lead&quot;&gt;${descriptions[s.page]}&lt;/p&gt;${error}${pane(s)}&lt;footer class=&quot;ms-footer&quot;&gt;&lt;div class=&quot;ms-status&quot; role=&quot;status&quot; aria-live=&quot;polite&quot;&gt;${esc(s.message||&#x27;예제 데이터 · 실제 파일 접근 없음&#x27;)}&lt;/div&gt;&lt;div class=&quot;ms-actions&quot;&gt;${idx&gt;0?&#x27;&lt;button class=&quot;ms-button cursor-interaction&quot; data-action=&quot;prev&quot;&gt;이전&lt;/button&gt;&#x27;:&#x27;&#x27;}${idx&lt;5?&#x27;&lt;button class=&quot;ms-button primary cursor-interaction&quot; data-action=&quot;next&quot;&gt;다음 단계 &#x27;+icon(&#x27;arrow-right&#x27;)+&#x27;&lt;/button&gt;&#x27;:&#x27;&#x27;}&lt;/div&gt;&lt;/footer&gt;&lt;/main&gt;${k===&#x27;studio&#x27;?&#x27;&lt;/div&gt;&#x27;:&#x27;&#x27;}&lt;div class=&quot;ms-bottom&quot;&gt;&lt;span&gt;프로젝트 변경 ${s.revision} · ${esc(s.version)}&lt;/span&gt;&lt;span&gt;실제 패키지 생성·서명·설치 없음&lt;/span&gt;&lt;/div&gt;`;
if(globalThis.lucide)globalThis.lucide.createIcons({attrs:{width:16,height:16}});
}
function runValidation(s){const err=validate(s);s.error=err?err[1]:&#x27;&#x27;;s.errorPage=err?err[0]:&#x27;&#x27;;s.validated=!err;s.message=err?&#x27;설정을 수정한 후 다시 검사해 주세요.&#x27;:&#x27;모의 설정 검사 통과 · SDK와 실제 파일 검사는 미실행&#x27;;return !err;}
root.addEventListener(&#x27;click&#x27;,e=&gt;{
const b=e.target.closest(&#x27;button&#x27;),app=e.target.closest(&#x27;[data-app]&#x27;);if(!b||!app)return;const k=app.dataset.app,s=state[k];
if(b.dataset.page){s.page=b.dataset.page;s.error=&#x27;&#x27;;s.errorPage=&#x27;&#x27;;}
const a=b.dataset.action,idx=pages.findIndex(p=&gt;p[0]===s.page);
if(a===&#x27;next&#x27;||a===&#x27;prev&#x27;){s.page=pages[Math.max(0,Math.min(5,idx+(a===&#x27;next&#x27;?1:-1)))][0];s.error=&#x27;&#x27;;s.errorPage=&#x27;&#x27;;}
if(a===&#x27;add-file&#x27;){s.extraFile=!s.extraFile;s.revision++;s.validated=false;s.build=&#x27;idle&#x27;;s.message=s.extraFile?&#x27;예제 파일을 추가했습니다.&#x27;:&#x27;추가한 예제 파일을 제거했습니다.&#x27;;}
if(a===&#x27;validate&#x27;)runValidation(s);
if(a===&#x27;build&#x27;){s.build=runValidation(s)?&#x27;done&#x27;:&#x27;failed&#x27;;if(s.build===&#x27;done&#x27;)s.message=&#x27;모의 빌드 완료 · 실제 산출물과 설치 증빙은 없습니다.&#x27;;}
if(a===&#x27;log&#x27;)s.log=!s.log;
render(k);persist();
});
root.addEventListener(&#x27;change&#x27;,e=&gt;{
const f=e.target.dataset.field,app=e.target.closest(&#x27;[data-app]&#x27;);if(!f||!app)return;const k=app.dataset.app,s=state[k];s[f]=e.target.type===&#x27;checkbox&#x27;?e.target.checked:e.target.value;s.revision++;s.validated=false;s.build=&#x27;idle&#x27;;s.error=&#x27;&#x27;;s.errorPage=&#x27;&#x27;;s.message=&#x27;설정이 변경되었습니다. 빌드 전 다시 검사합니다.&#x27;;render(k);persist();
});
window.addEventListener(&#x27;openai:set_globals&#x27;,e=&gt;{const saved=e.detail?.globals?.widgetState?.privateContent;if(!saved||saved.schema!==1||saved.sessionId===sessionId)return;restore(saved);render(&#x27;studio&#x27;);render(&#x27;guided&#x27;);});
render(&#x27;studio&#x27;);render(&#x27;guided&#x27;);
if(globalThis.Tweak){[&#x27;studio&#x27;,&#x27;guided&#x27;].forEach(k=&gt;{const app=root.querySelector(`[data-app=&quot;${k}&quot;]`),design={corners:12};const tweak=new Tweak({container:app,onChange:()=&gt;{app.style.borderRadius=design.corners+&#x27;px&#x27;;}});tweak.addSlider(design,&#x27;corners&#x27;,{label:&#x27;창 모서리&#x27;,min:0,max:20,step:2,unit:&#x27;px&#x27;});});}
})();
&lt;/script&gt;
&lt;/div&gt;
&lt;script src=&quot;https://unpkg.com/@floating-ui/core@1.7.3/dist/floating-ui.core.umd.min.js&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;https://unpkg.com/@floating-ui/dom@1.7.4/dist/floating-ui.dom.umd.min.js&quot;&gt;&lt;/script&gt;
&lt;script&gt;
(() =&gt; {
if (window.FloatingUIDOM == null) {
return;
}
const { autoUpdate, computePosition, flip, offset, shift, size } =
window.FloatingUIDOM;
const tooltipPlacements = new Set([&quot;top&quot;, &quot;right&quot;, &quot;bottom&quot;, &quot;left&quot;]);
const tooltipId = `codex-visualization-tooltip-${
window.crypto?.randomUUID?.() ?? Date.now().toString(36)
}`;
const tooltipGap = 5;
const hoverDelay = 700;
const skipDelay = 300;
let activeTrigger = null;
let pendingTrigger = null;
let tooltip = null;
let openTimeout = null;
let autoUpdateCleanup = null;
let positionRequest = 0;
let skipDelayUntil = 0;
let lastPointerWasTouch = false;
let pinnedByTouch = false;
const getTrigger = (event) =&gt; {
for (const target of event.composedPath()) {
if (target?.nodeType === Node.ELEMENT_NODE) {
const trigger = target.closest(&quot;[data-tooltip]&quot;);
if (trigger) return trigger;
}
}
return null;
};
const containsTarget = (trigger, target) =&gt;
target?.nodeType != null &amp;&amp; trigger.contains(target);
const getContent = (trigger) =&gt;
trigger.getAttribute(&quot;data-tooltip&quot;)?.trim() ?? &quot;&quot;;
const getPlacement = (trigger) =&gt; {
const placement = trigger.getAttribute(&quot;data-tooltip-placement&quot;);
return tooltipPlacements.has(placement) ? placement : &quot;top&quot;;
};
const getTooltip = () =&gt; {
if (tooltip == null) {
tooltip = document.createElement(&quot;div&quot;);
tooltip.id = tooltipId;
tooltip.className = &quot;tooltip&quot;;
tooltip.setAttribute(&quot;role&quot;, &quot;tooltip&quot;);
}
return tooltip;
};
const setDescribedBy = (trigger, described) =&gt; {
const ids = new Set(
(trigger.getAttribute(&quot;aria-describedby&quot;) ?? &quot;&quot;)
.split(/\s+/)
.filter(Boolean),
);
if (described) {
ids.add(tooltipId);
} else {
ids.delete(tooltipId);
}
if (ids.size === 0) {
trigger.removeAttribute(&quot;aria-describedby&quot;);
} else {
trigger.setAttribute(&quot;aria-describedby&quot;, [...ids].join(&quot; &quot;));
}
};
const clearOpenTimeout = () =&gt; {
if (openTimeout != null) {
window.clearTimeout(openTimeout);
openTimeout = null;
}
pendingTrigger = null;
};
const closeTooltip = (activateSkipDelay = true) =&gt; {
clearOpenTimeout();
pinnedByTouch = false;
positionRequest += 1;
autoUpdateCleanup?.();
autoUpdateCleanup = null;
if (activeTrigger == null) {
return;
}
setDescribedBy(activeTrigger, false);
activeTrigger = null;
tooltip?.remove();
if (activateSkipDelay) {
skipDelayUntil = Date.now() + skipDelay;
}
};
const updatePosition = (trigger, floating) =&gt; {
const request = ++positionRequest;
void computePosition(trigger, floating, {
middleware: [
offset(tooltipGap),
flip({ padding: tooltipGap }),
shift({ padding: tooltipGap }),
size({
padding: tooltipGap,
apply({ availableHeight, availableWidth, elements }) {
elements.floating.style.setProperty(
&quot;--tooltip-available-width&quot;,
`${Math.max(0, availableWidth)}px`,
);
elements.floating.style.setProperty(
&quot;--tooltip-available-height&quot;,
`${Math.max(0, availableHeight)}px`,
);
},
}),
],
placement: getPlacement(trigger),
strategy: &quot;fixed&quot;,
})
.then(({ x, y }) =&gt; {
if (activeTrigger !== trigger || request !== positionRequest) {
return;
}
const scale = window.devicePixelRatio || 1;
floating.style.transform = `translate(${Math.round(x * scale) / scale}px, ${Math.round(y * scale) / scale}px)`;
floating.style.visibility = &quot;visible&quot;;
})
.catch(() =&gt; {
if (activeTrigger === trigger &amp;&amp; request === positionRequest) {
closeTooltip(false);
}
});
};
const openTooltip = (trigger) =&gt; {
const content = getContent(trigger);
if (!trigger.isConnected || content.length === 0) {
return;
}
clearOpenTimeout();
if (activeTrigger === trigger) {
return;
}
closeTooltip();
const floating = getTooltip();
floating.textContent = content;
floating.style.visibility = &quot;hidden&quot;;
floating.style.transform = &quot;translate(0, 0)&quot;;
document.body.appendChild(floating);
activeTrigger = trigger;
setDescribedBy(trigger, true);
autoUpdateCleanup = autoUpdate(trigger, floating, () =&gt; {
updatePosition(trigger, floating);
});
};
const requestOpen = (trigger, immediate = false) =&gt; {
if (
getContent(trigger).length === 0 ||
activeTrigger === trigger ||
pendingTrigger === trigger
) {
return;
}
clearOpenTimeout();
pendingTrigger = trigger;
const delay =
immediate || activeTrigger != null || Date.now() &lt; skipDelayUntil
? 0
: hoverDelay;
if (delay === 0) {
openTooltip(trigger);
return;
}
openTimeout = window.setTimeout(() =&gt; {
openTimeout = null;
pendingTrigger = null;
openTooltip(trigger);
}, delay);
};
const handlePointerOpen = (event) =&gt; {
if (event.pointerType === &quot;touch&quot;) {
return;
}
const trigger = getTrigger(event);
if (trigger == null || containsTarget(trigger, event.relatedTarget)) {
return;
}
requestOpen(trigger);
};
const handlePointer = (event) =&gt; {
lastPointerWasTouch =
event.type === &quot;pointerdown&quot; &amp;&amp; event.pointerType === &quot;touch&quot;;
};
const handleTouchClick = (event) =&gt; {
const isTouch = event.pointerType === &quot;touch&quot; || lastPointerWasTouch;
lastPointerWasTouch = false;
if (!isTouch || event.detail === 0) {
return;
}
const trigger = getTrigger(event);
if (
trigger == null ||
!trigger.isConnected ||
getContent(trigger).length === 0 ||
(pinnedByTouch &amp;&amp; activeTrigger === trigger)
) {
if (pinnedByTouch) {
closeTooltip();
}
return;
}
openTooltip(trigger);
pinnedByTouch = activeTrigger === trigger;
window.setTimeout(() =&gt; {
if (activeTrigger === trigger &amp;&amp; !trigger.isConnected) {
closeTooltip(false);
}
}, 0);
};
const handleLeave = (event) =&gt; {
if (pinnedByTouch) {
return;
}
const trigger = getTrigger(event);
if (trigger == null || containsTarget(trigger, event.relatedTarget)) {
return;
}
if (activeTrigger === trigger || pendingTrigger === trigger) {
closeTooltip();
}
};
const handleFocusIn = (event) =&gt; {
const trigger = getTrigger(event);
if (trigger?.matches(&quot;:focus-visible&quot;)) {
requestOpen(trigger, true);
}
};
const handleKeyDown = (event) =&gt; {
if (event.key === &quot;Escape&quot;) {
closeTooltip();
}
};
const handleContextMenu = (event) =&gt; {
lastPointerWasTouch = false;
const trigger = getTrigger(event);
if (activeTrigger === trigger || pendingTrigger === trigger) {
closeTooltip();
}
};
const handlers = {
pointerover: handlePointerOpen,
pointerdown: handlePointer,
pointercancel: handlePointer,
click: handleTouchClick,
pointerout: handleLeave,
focusin: handleFocusIn,
focusout: handleLeave,
keydown: handleKeyDown,
contextmenu: handleContextMenu,
};
const roots = new Map();
const handledEvents = new WeakSet();
const handleEvent = (event) =&gt; {
if (handledEvents.has(event)) return;
handledEvents.add(event);
handlers[event.type](event);
};
const discoverRoots = (node) =&gt; {
if (node.shadowRoot) listen(node.shadowRoot);
for (const element of node.querySelectorAll(&quot;*&quot;)) {
if (element.shadowRoot) listen(element.shadowRoot);
}
};
const listen = (root) =&gt; {
if (roots.has(root)) return;
for (const type of Object.keys(handlers)) root.addEventListener(type, handleEvent);
const observer = new window.MutationObserver((records) =&gt; {
for (const { addedNodes } of records) {
for (const node of addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) discoverRoots(node);
}
}
for (const [root, cleanup] of roots) {
if (root.host &amp;&amp; !root.host.isConnected) {
cleanup();
roots.delete(root);
}
}
if (activeTrigger &amp;&amp; !activeTrigger.isConnected) closeTooltip(false);
});
observer.observe(root, { childList: true, subtree: true });
roots.set(root, () =&gt; {
observer.disconnect();
for (const type of Object.keys(handlers)) root.removeEventListener(type, handleEvent);
});
discoverRoots(root);
};
const destroy = () =&gt; {
closeTooltip(false);
lastPointerWasTouch = false;
for (const cleanup of roots.values()) cleanup();
roots.clear();
tooltip = null;
};
// Events between children of one shadow root never reach the document.
listen(document);
window.addEventListener(&quot;pagehide&quot;, destroy, { once: true });
})();
&lt;/script&gt;
&lt;script id=&quot;codex-visualization-tabs&quot;&gt;
(() =&gt; {
const selector = &#x27;.nav[role=&quot;tablist&quot;] [role=&quot;tab&quot;]&#x27;;
const disabled = &#x27;:disabled,[aria-disabled=&quot;true&quot;]&#x27;;
const selectTab = (selected) =&gt; {
const panelId = selected.getAttribute(&quot;aria-controls&quot;);
for (const tab of selected.closest(&#x27;[role=&quot;tablist&quot;]&#x27;).querySelectorAll(&#x27;[role=&quot;tab&quot;]&#x27;)) {
const active = tab === selected;
tab.classList.toggle(&quot;active&quot;, active);
tab.setAttribute(&quot;aria-selected&quot;, String(active));
const id = tab.getAttribute(&quot;aria-controls&quot;);
const panel = id &amp;&amp; document.getElementById(id);
if (panel != null) {
panel.hidden = id !== panelId;
if (!panel.hidden &amp;&amp; selected.id) {
panel.setAttribute(&quot;aria-labelledby&quot;, selected.id);
}
}
}
};
const interact = (event) =&gt; {
const tab = event.target?.closest?.(selector);
if (event.defaultPrevented || tab == null || tab.matches(disabled)) {
return;
}
if (event.type === &quot;click&quot;) {
selectTab(tab);
return;
}
const tabs = Array.from(
tab.closest(&#x27;[role=&quot;tablist&quot;]&#x27;).querySelectorAll(&#x27;[role=&quot;tab&quot;]&#x27;),
).filter((candidate) =&gt; !candidate.matches(disabled));
const current = tabs.indexOf(tab);
let next;
if (event.key === &quot;ArrowRight&quot; || event.key === &quot;ArrowDown&quot;) {
next = (current + 1) % tabs.length;
} else if (event.key === &quot;ArrowLeft&quot; || event.key === &quot;ArrowUp&quot;) {
next = (current + tabs.length - 1) % tabs.length;
} else if (event.key === &quot;Home&quot;) {
next = 0;
} else if (event.key === &quot;End&quot;) {
next = tabs.length - 1;
} else {
return;
}
event.preventDefault();
tabs[next].focus();
selectTab(tabs[next]);
};
for (const list of document.querySelectorAll(&#x27;.nav[role=&quot;tablist&quot;]&#x27;)) {
const selected =
list.querySelector(&#x27;[role=&quot;tab&quot;][aria-selected=&quot;true&quot;]&#x27;) ??
list.querySelector(&#x27;[role=&quot;tab&quot;].active&#x27;) ??
list.querySelector(&#x27;[role=&quot;tab&quot;]&#x27;);
if (selected != null) {
selectTab(selected);
}
}
document.addEventListener(&quot;click&quot;, interact);
document.addEventListener(&quot;keydown&quot;, interact);
window.addEventListener(&quot;pagehide&quot;, () =&gt; {
document.removeEventListener(&quot;click&quot;, interact);
document.removeEventListener(&quot;keydown&quot;, interact);
}, { once: true });
})();
&lt;/script&gt;
&lt;script id=&quot;codex-visualization-carousel&quot;&gt;
(() =&gt; {
for (const root of document.querySelectorAll(&quot;.viz-carousel&quot;)) {
const panels = Array.from(
root.querySelectorAll(&quot;:scope &gt; [data-variant]&quot;),
);
if (panels.length === 0) {
continue;
}
root.setAttribute(&quot;role&quot;, &quot;group&quot;);
panels.forEach((panel, index) =&gt; {
panel.hidden = index !== 0;
panel.setAttribute(&quot;role&quot;, &quot;group&quot;);
if (!panel.hasAttribute(&quot;aria-label&quot;)) {
panel.setAttribute(&quot;aria-label&quot;, panel.dataset.variant);
}
});
root.dataset.activeVariant = panels[0].dataset.variant;
if (panels.length === 1) {
continue;
}
const controls = document.createElement(&quot;div&quot;);
controls.className = &quot;viz-carousel-controls&quot;;
// Portable copy of the settings dropdown&#x27;s legacy glyph; no 14px icon-package preset matches it.
// https://github.com/openai/openai/blob/master/codex/codex-apps/webview/src/icons/chevron.svg
controls.innerHTML = `
&lt;button type=&quot;button&quot; class=&quot;viz-carousel-previous cursor-interaction&quot;&gt;&lt;i data-lucide=&quot;chevron-left&quot; aria-hidden=&quot;true&quot;&gt;&lt;/i&gt;&lt;/button&gt;
&lt;details class=&quot;viz-carousel-picker&quot;&gt;
&lt;summary class=&quot;cursor-interaction&quot;&gt;
&lt;span class=&quot;viz-carousel-count tabular-nums&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;viz-carousel-name&quot;&gt;&lt;/span&gt;
&lt;svg class=&quot;viz-carousel-caret&quot; width=&quot;14&quot; height=&quot;14&quot; viewBox=&quot;0 0 20 21&quot; fill=&quot;none&quot; aria-hidden=&quot;true&quot; focusable=&quot;false&quot;&gt;
&lt;path d=&quot;M15.2793 7.71101C15.539 7.45131 15.961 7.45131 16.2207 7.71101C16.4804 7.97071 16.4804 8.39272 16.2207 8.65242L10.4707 14.4024C10.211 14.6621 9.78902 14.6621 9.52932 14.4024L3.77932 8.65242L3.69436 8.54792C3.52385 8.28979 3.55205 7.93828 3.77932 7.71101C4.00659 7.48374 4.3581 7.45554 4.61623 7.62605L4.72073 7.71101L10 12.9903L15.2793 7.71101Z&quot; fill=&quot;currentColor&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;0.6&quot; /&gt;
&lt;/svg&gt;
&lt;/summary&gt;
&lt;div class=&quot;viz-carousel-options&quot;&gt;&lt;/div&gt;
&lt;/details&gt;
&lt;button type=&quot;button&quot; class=&quot;viz-carousel-next cursor-interaction&quot;&gt;&lt;i data-lucide=&quot;chevron-right&quot; aria-hidden=&quot;true&quot;&gt;&lt;/i&gt;&lt;/button&gt;`;
const previous = controls.querySelector(&quot;.viz-carousel-previous&quot;);
const next = controls.querySelector(&quot;.viz-carousel-next&quot;);
const picker = controls.querySelector(&quot;.viz-carousel-picker&quot;);
const summary = picker.querySelector(&quot;summary&quot;);
const count = controls.querySelector(&quot;.viz-carousel-count&quot;);
const name = controls.querySelector(&quot;.viz-carousel-name&quot;);
const options = controls.querySelector(&quot;.viz-carousel-options&quot;);
const status = document.createElement(&quot;span&quot;);
status.className = &quot;sr-only&quot;;
status.setAttribute(&quot;role&quot;, &quot;status&quot;);
status.setAttribute(&quot;aria-live&quot;, &quot;polite&quot;);
status.setAttribute(&quot;aria-atomic&quot;, &quot;true&quot;);
previous.setAttribute(
&quot;aria-label&quot;,
root.dataset.previousLabel ?? &quot;Previous variant&quot;,
);
next.setAttribute(&quot;aria-label&quot;, root.dataset.nextLabel ?? &quot;Next variant&quot;);
let active = 0;
let mousePointerId = null;
let summaryPointerHandled = false;
summary.addEventListener(&quot;pointerdown&quot;, (event) =&gt; {
summaryPointerHandled =
event.pointerType === &quot;mouse&quot; &amp;&amp; event.button === 0 &amp;&amp; !event.ctrlKey;
if (!summaryPointerHandled) {
return;
}
event.preventDefault();
summary.focus({ preventScroll: true });
picker.open = !picker.open;
mousePointerId = picker.open ? event.pointerId : null;
});
summary.addEventListener(&quot;click&quot;, (event) =&gt; {
if (summaryPointerHandled &amp;&amp; event.detail &gt; 0) {
event.preventDefault();
}
});
const choices = panels.map((panel, index) =&gt; {
const choice = document.createElement(&quot;button&quot;);
choice.type = &quot;button&quot;;
choice.className = &quot;cursor-interaction&quot;;
choice.textContent = panel.dataset.variant;
choice.addEventListener(&quot;click&quot;, () =&gt; {
select(index);
summary.focus();
});
choice.addEventListener(&quot;pointerup&quot;, (event) =&gt; {
if (
picker.open &amp;&amp;
event.pointerType === &quot;mouse&quot; &amp;&amp;
event.button === 0 &amp;&amp;
event.pointerId === mousePointerId
) {
choice.click();
}
});
options.appendChild(choice);
return choice;
});
const select = (index) =&gt; {
const changed = active !== index;
active = index;
panels.forEach((panel, position) =&gt; {
panel.hidden = position !== active;
choices[position].setAttribute(
&quot;aria-current&quot;,
String(position === active),
);
});
root.dataset.activeVariant = panels[active].dataset.variant;
count.textContent = `${active + 1} / ${panels.length}`;
name.textContent = panels[active].dataset.variant;
picker.open = false;
mousePointerId = null;
if (changed) {
status.textContent = `${count.textContent}: ${name.textContent}`;
}
};
previous.addEventListener(&quot;click&quot;, () =&gt;
select((active + panels.length - 1) % panels.length),
);
next.addEventListener(&quot;click&quot;, () =&gt;
select((active + 1) % panels.length),
);
picker.addEventListener(&quot;keydown&quot;, (event) =&gt; {
if (event.key === &quot;Escape&quot; &amp;&amp; picker.open) {
event.preventDefault();
picker.open = false;
mousePointerId = null;
summary.focus();
return;
}
if (event.altKey || event.ctrlKey || event.metaKey) {
return;
}
const index = choices.indexOf(document.activeElement);
let nextIndex;
if (event.key === &quot;ArrowDown&quot;) {
nextIndex = index &lt; 0 ? active : (index + 1) % choices.length;
} else if (event.key === &quot;ArrowUp&quot;) {
nextIndex = index &lt; 0 ? active : (index + choices.length - 1) % choices.length;
} else if (picker.open &amp;&amp; event.key === &quot;Home&quot;) {
nextIndex = 0;
} else if (picker.open &amp;&amp; event.key === &quot;End&quot;) {
nextIndex = choices.length - 1;
} else {
return;
}
event.preventDefault();
picker.open = true;
choices[nextIndex].focus();
});
picker.addEventListener(&quot;focusout&quot;, (event) =&gt; {
if (!picker.contains(event.relatedTarget)) {
picker.open = false;
mousePointerId = null;
}
});
const dismiss = (event) =&gt; {
if (!summary.contains(event.target)) {
mousePointerId = null;
}
if (!picker.contains(event.target)) {
picker.open = false;
}
};
const endPointerSelection = () =&gt; {
mousePointerId = null;
};
const sizePicker = () =&gt; {
root.style.setProperty(
&quot;--viz-carousel-picker-height&quot;,
`${picker.open ? options.offsetHeight : 0}px`,
);
};
picker.addEventListener(&quot;toggle&quot;, sizePicker);
window.addEventListener(&quot;resize&quot;, sizePicker);
document.addEventListener(&quot;pointerdown&quot;, dismiss);
document.addEventListener(&quot;pointerup&quot;, endPointerSelection);
document.addEventListener(&quot;pointercancel&quot;, endPointerSelection);
window.addEventListener(&quot;blur&quot;, endPointerSelection);
window.addEventListener(
&quot;pagehide&quot;,
() =&gt; {
document.removeEventListener(&quot;pointerdown&quot;, dismiss);
document.removeEventListener(&quot;pointerup&quot;, endPointerSelection);
document.removeEventListener(&quot;pointercancel&quot;, endPointerSelection);
window.removeEventListener(&quot;blur&quot;, endPointerSelection);
window.removeEventListener(&quot;resize&quot;, sizePicker);
},
{ once: true },
);
root.append(controls, status);
select(0);
}
})();
&lt;/script&gt;
&lt;script id=&quot;codex-visualization-lucide&quot; async src=&quot;https://unpkg.com/lucide@1.17.0/dist/umd/lucide.js&quot;&gt;&lt;/script&gt;
&lt;script&gt;
(() =&gt; {
const initialize = () =&gt; {
globalThis.lucide?.createIcons({ attrs: { width: 16, height: 16 } });
};
if (globalThis.lucide != null) {
initialize();
return;
}
document
.getElementById(&quot;codex-visualization-lucide&quot;)
?.addEventListener(&quot;load&quot;, initialize, { once: true });
})();
&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
"></iframe>
<script>(() => {
const frame = document.getElementById("codex-visualization");
if (!(frame instanceof HTMLIFrameElement)) {
return;
}
const isShare = document.documentElement.classList.contains(
"visualization-share-root",
);
let viewportHeight = globalThis.innerHeight;
let sizeFingerprint = null;
let sizeWriteTimeout;
let lastSize = null;
let pendingSize = null;
const storageKey = `codex:visualization-widget-state-v2:${JSON.stringify([
globalThis.location.pathname,
globalThis.location.search,
])}`;
const sizeStorageKey = `${storageKey}:share-size-v1`;
const parseState = (serializedState) => {
if (
typeof serializedState !== "string" ||
new TextEncoder().encode(serializedState).length > 16 * 1024
) {
throw new TypeError(
"Widget state must be a JSON object of at most 16 KiB",
);
}
/** @type {unknown} */
const state = JSON.parse(serializedState);
if (
state == null ||
typeof state !== "object" ||
Array.isArray(state) ||
("imageIds" in state &&
(!Array.isArray(state.imageIds) || state.imageIds.length > 0))
) {
throw new TypeError("Widget state must be a JSON object");
}
const storedState = { modelContent: null, privateContent: null, ...state };
const canonicalState = JSON.stringify(
storedState,
(_key, /** @type {unknown} */ value) => {
if (typeof value === "number" && !Number.isFinite(value)) {
throw new TypeError("Widget state must contain finite numbers");
}
return value;
},
);
if (new TextEncoder().encode(canonicalState).length > 16 * 1024) {
throw new TypeError(
"Widget state must be a JSON object of at most 16 KiB",
);
}
return storedState;
};
let storage = null;
let state = null;
try {
const localStorage = globalThis.localStorage;
const probeKey = `${storageKey}:probe`;
localStorage.setItem(probeKey, "1");
localStorage.removeItem(probeKey);
storage = localStorage;
const savedState = storage.getItem(storageKey);
if (savedState != null) {
try {
state = parseState(savedState);
} catch {
// Invalid saved state does not disable future writes.
}
}
} catch {
storage = null;
}
const persistSize = () => {
globalThis.clearTimeout(sizeWriteTimeout);
if (pendingSize == null) {
return;
}
try {
storage?.setItem(sizeStorageKey, pendingSize);
} catch {
// A denied or full cache must not affect the visualization.
}
pendingSize = null;
};
if (isShare) {
globalThis.addEventListener("pagehide", persistSize);
}
const initialGlobals = {
widgetState: state,
statePersistence: storage == null ? "none" : "local",
};
const channel = new MessageChannel();
channel.port1.addEventListener("message", (event) => {
const data = event.data;
if (typeof data !== "object" || data == null) {
return;
}
if (data.type === "widget-state-write") {
if (!Number.isSafeInteger(data.id) || data.id <= 0) {
return;
}
let ok = false;
try {
if (storage != null) {
const state = parseState(data.state);
storage.setItem(storageKey, JSON.stringify(state));
ok = true;
}
} catch {
// Storage denial, quota limits, and invalid state return a failed receipt.
}
channel.port1.postMessage({
type: "widget-state-result",
id: data.id,
ok,
});
return;
}
if (data.type === "height") {
const height = data.height;
if (
typeof height !== "number" ||
!Number.isFinite(height) ||
height < 0
) {
return;
}
if (height <= 10_000) {
const width = isShare && storage != null ? frame.clientWidth : null;
frame.style.height = Math.ceil(height) + "px";
if (isShare && storage != null) {
const size = JSON.stringify({
fingerprint: sizeFingerprint,
width,
viewportHeight: globalThis.innerHeight,
dark: globalThis.matchMedia("(prefers-color-scheme: dark)").matches,
scale: globalThis.devicePixelRatio,
height: Math.ceil(height),
});
if (size !== lastSize) {
lastSize = size;
pendingSize = size;
globalThis.clearTimeout(sizeWriteTimeout);
sizeWriteTimeout = globalThis.setTimeout(persistSize, 100);
}
}
}
if (isShare) {
// Keep unusually tall content accessible without accepting an unbounded height.
if (height > 10_000 && Number.parseFloat(frame.style.height) === 0) {
frame.style.removeProperty("height");
}
frame.scrolling = height > 10_000 ? "auto" : "no";
document.documentElement.classList.add("visualization-share-ready");
}
return;
}
if (data.type === "scroll-to") {
const top = data.top;
if (
typeof top === "number" &&
Number.isFinite(top) &&
top >= 0 &&
globalThis.navigator.userActivation?.isActive === true
) {
globalThis.scrollTo({
top: Math.ceil(
globalThis.scrollY + frame.getBoundingClientRect().top + top,
),
});
}
return;
}
if (data.type === "open-external") {
const rawHref = data.href;
if (typeof rawHref !== "string") {
return;
}
let href;
try {
const url = new URL(rawHref, document.baseURI);
if (url.protocol !== "https:") {
return;
}
href = url.href;
} catch {
return;
}
if (globalThis.navigator.userActivation?.isActive === true) {
const anchor = document.createElement("a");
anchor.href = href;
anchor.rel = "noopener noreferrer";
anchor.target = "_blank";
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
}
return;
}
if (data.type === "follow-up") {
const prompt = data.prompt;
if (
typeof prompt !== "string" ||
globalThis.navigator.userActivation?.isActive !== true
) {
return;
}
const title = data.title;
globalThis.prompt(typeof title === "string" ? title : prompt, prompt);
}
});
channel.port1.start();
const srcdoc = frame.dataset.srcdoc;
if (srcdoc == null) {
return;
}
globalThis.addEventListener("resize", () => {
const height = Number.parseFloat(frame.style.height);
if (!isShare && (!Number.isFinite(height) || height <= viewportHeight)) {
frame.style.removeProperty("height");
}
viewportHeight = globalThis.innerHeight;
channel.port1.postMessage({ type: "measure" });
});
frame.addEventListener(
"load",
() => {
frame.contentWindow?.postMessage(
{ type: "codex-visualization-initialize" },
"*",
[channel.port2],
);
},
{ once: true },
);
// Match the trusted bootstrap element, not the same text in an authored title.
const frameHtml = srcdoc.replace(
'<script type="application/json" id="codex-visualization-widget-state">__CODEX_VISUALIZATION_WIDGET_STATE__<\/script>',
() =>
'<script type="application/json" id="codex-visualization-widget-state">' +
JSON.stringify(initialGlobals).replaceAll("<", "\\u003c") +
"<\/script>",
);
if (isShare && storage != null) {
// Fingerprint the rendered document, including restored widget state, without
// storing its content. This is a layout cache, not a security identity.
let hash = 5381;
for (let index = 0; index < frameHtml.length; index += 1) {
hash = (hash * 33 + frameHtml.charCodeAt(index)) % 4_294_967_296;
}
sizeFingerprint = hash.toString(36);
try {
const size = JSON.parse(storage.getItem(sizeStorageKey) ?? "null");
if (
size?.fingerprint === sizeFingerprint &&
size.width === frame.clientWidth &&
size.viewportHeight === globalThis.innerHeight &&
size.dark ===
globalThis.matchMedia("(prefers-color-scheme: dark)").matches &&
size.scale === globalThis.devicePixelRatio &&
typeof size.height === "number" &&
Number.isFinite(size.height) &&
size.height >= 0 &&
size.height <= 10_000
) {
frame.style.height = size.height + "px";
}
} catch {
// Ignore missing or invalid cache entries and measure the live frame.
}
}
frame.srcdoc = frameHtml;
delete frame.dataset.srcdoc;
})();
</script>
</body>
</html>