webui: convert sketch-app-shell to use tailwind
Doing the conversion from shadowDOM to tailwind starting with the
outermost elements in the page because shadowDOM hides dom nodes from
ancestor styles, which means tailwind styles don't apply to an element
if its ancestors use shadowDOM.
- add new SketchTailwindElement that disables shadowDOM for tailwind
- convert sketch-app-shell to inherit from SketchTailwindElement
- convert sketch-app-shell's CSS from shadowDOM to tailwind classes
diff --git a/webui/src/global.css b/webui/src/global.css
new file mode 100644
index 0000000..f1d8c73
--- /dev/null
+++ b/webui/src/global.css
@@ -0,0 +1 @@
+@import "tailwindcss";
diff --git a/webui/src/sketch-app-shell.html b/webui/src/sketch-app-shell.html
index 8127fa2..bf1fd13 100644
--- a/webui/src/sketch-app-shell.html
+++ b/webui/src/sketch-app-shell.html
@@ -7,6 +7,7 @@
<link rel="stylesheet" href="static/sketch-app-shell.css" />
<script src="static/sketch-app-shell.js" async type="module"></script>
<script src="static/interface-detection.js"></script>
+ <link rel="stylesheet" href="static/tailwind.css" />
</head>
<body>
<sketch-app-shell></sketch-app-shell>
diff --git a/webui/src/tailwind.css b/webui/src/tailwind.css
new file mode 100644
index 0000000..05a41c8
--- /dev/null
+++ b/webui/src/tailwind.css
@@ -0,0 +1,1332 @@
+/*! tailwindcss v4.1.8 | MIT License | https://tailwindcss.com */
+@layer properties;
+@layer theme, base, components, utilities;
+@layer theme {
+ :root,
+ :host {
+ --font-sans:
+ ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
+ "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ --font-mono:
+ ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
+ "Courier New", monospace;
+ --color-red-300: oklch(80.8% 0.114 19.571);
+ --color-red-600: oklch(57.7% 0.245 27.325);
+ --color-red-700: oklch(50.5% 0.213 27.518);
+ --color-orange-50: oklch(98% 0.016 73.684);
+ --color-orange-500: oklch(70.5% 0.213 47.604);
+ --color-orange-800: oklch(47% 0.157 37.304);
+ --color-green-600: oklch(62.7% 0.194 149.214);
+ --color-blue-50: oklch(97% 0.014 254.604);
+ --color-blue-100: oklch(93.2% 0.032 255.585);
+ --color-blue-400: oklch(70.7% 0.165 254.624);
+ --color-blue-500: oklch(62.3% 0.214 259.815);
+ --color-blue-600: oklch(54.6% 0.245 262.881);
+ --color-blue-700: oklch(48.8% 0.243 264.376);
+ --color-blue-800: oklch(42.4% 0.199 265.638);
+ --color-gray-50: oklch(98.5% 0.002 247.839);
+ --color-gray-100: oklch(96.7% 0.003 264.542);
+ --color-gray-200: oklch(92.8% 0.006 264.531);
+ --color-gray-300: oklch(87.2% 0.01 258.338);
+ --color-gray-400: oklch(70.7% 0.022 261.325);
+ --color-gray-500: oklch(55.1% 0.027 264.364);
+ --color-gray-600: oklch(44.6% 0.03 256.802);
+ --color-gray-700: oklch(37.3% 0.034 259.733);
+ --color-gray-800: oklch(27.8% 0.033 256.848);
+ --color-white: #fff;
+ --spacing: 0.25rem;
+ --container-6xl: 72rem;
+ --text-xs: 0.75rem;
+ --text-xs--line-height: calc(1 / 0.75);
+ --text-sm: 0.875rem;
+ --text-sm--line-height: calc(1.25 / 0.875);
+ --text-base: 1rem;
+ --text-base--line-height: calc(1.5 / 1);
+ --text-lg: 1.125rem;
+ --text-lg--line-height: calc(1.75 / 1.125);
+ --font-weight-normal: 400;
+ --font-weight-medium: 500;
+ --font-weight-semibold: 600;
+ --font-weight-bold: 700;
+ --leading-relaxed: 1.625;
+ --radius-lg: 0.5rem;
+ --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
+ --animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+ --default-transition-duration: 150ms;
+ --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ --default-font-family: var(--font-sans);
+ --default-mono-font-family: var(--font-mono);
+ }
+}
+@layer base {
+ *,
+ ::after,
+ ::before,
+ ::backdrop,
+ ::file-selector-button {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+ border: 0 solid;
+ }
+ html,
+ :host {
+ line-height: 1.5;
+ -webkit-text-size-adjust: 100%;
+ tab-size: 4;
+ font-family: var(
+ --default-font-family,
+ ui-sans-serif,
+ system-ui,
+ sans-serif,
+ "Apple Color Emoji",
+ "Segoe UI Emoji",
+ "Segoe UI Symbol",
+ "Noto Color Emoji"
+ );
+ font-feature-settings: var(--default-font-feature-settings, normal);
+ font-variation-settings: var(--default-font-variation-settings, normal);
+ -webkit-tap-highlight-color: transparent;
+ }
+ hr {
+ height: 0;
+ color: inherit;
+ border-top-width: 1px;
+ }
+ abbr:where([title]) {
+ -webkit-text-decoration: underline dotted;
+ text-decoration: underline dotted;
+ }
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ font-size: inherit;
+ font-weight: inherit;
+ }
+ a {
+ color: inherit;
+ -webkit-text-decoration: inherit;
+ text-decoration: inherit;
+ }
+ b,
+ strong {
+ font-weight: bolder;
+ }
+ code,
+ kbd,
+ samp,
+ pre {
+ font-family: var(
+ --default-mono-font-family,
+ ui-monospace,
+ SFMono-Regular,
+ Menlo,
+ Monaco,
+ Consolas,
+ "Liberation Mono",
+ "Courier New",
+ monospace
+ );
+ font-feature-settings: var(--default-mono-font-feature-settings, normal);
+ font-variation-settings: var(
+ --default-mono-font-variation-settings,
+ normal
+ );
+ font-size: 1em;
+ }
+ small {
+ font-size: 80%;
+ }
+ sub,
+ sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+ }
+ sub {
+ bottom: -0.25em;
+ }
+ sup {
+ top: -0.5em;
+ }
+ table {
+ text-indent: 0;
+ border-color: inherit;
+ border-collapse: collapse;
+ }
+ :-moz-focusring {
+ outline: auto;
+ }
+ progress {
+ vertical-align: baseline;
+ }
+ summary {
+ display: list-item;
+ }
+ ol,
+ ul,
+ menu {
+ list-style: none;
+ }
+ img,
+ svg,
+ video,
+ canvas,
+ audio,
+ iframe,
+ embed,
+ object {
+ display: block;
+ vertical-align: middle;
+ }
+ img,
+ video {
+ max-width: 100%;
+ height: auto;
+ }
+ button,
+ input,
+ select,
+ optgroup,
+ textarea,
+ ::file-selector-button {
+ font: inherit;
+ font-feature-settings: inherit;
+ font-variation-settings: inherit;
+ letter-spacing: inherit;
+ color: inherit;
+ border-radius: 0;
+ background-color: transparent;
+ opacity: 1;
+ }
+ :where(select:is([multiple], [size])) optgroup {
+ font-weight: bolder;
+ }
+ :where(select:is([multiple], [size])) optgroup option {
+ padding-inline-start: 20px;
+ }
+ ::file-selector-button {
+ margin-inline-end: 4px;
+ }
+ ::placeholder {
+ opacity: 1;
+ }
+ @supports (not (-webkit-appearance: -apple-pay-button)) or
+ (contain-intrinsic-size: 1px) {
+ ::placeholder {
+ color: currentcolor;
+ @supports (color: color-mix(in lab, red, red)) {
+ color: color-mix(in oklab, currentcolor 50%, transparent);
+ }
+ }
+ }
+ textarea {
+ resize: vertical;
+ }
+ ::-webkit-search-decoration {
+ -webkit-appearance: none;
+ }
+ ::-webkit-date-and-time-value {
+ min-height: 1lh;
+ text-align: inherit;
+ }
+ ::-webkit-datetime-edit {
+ display: inline-flex;
+ }
+ ::-webkit-datetime-edit-fields-wrapper {
+ padding: 0;
+ }
+ ::-webkit-datetime-edit,
+ ::-webkit-datetime-edit-year-field,
+ ::-webkit-datetime-edit-month-field,
+ ::-webkit-datetime-edit-day-field,
+ ::-webkit-datetime-edit-hour-field,
+ ::-webkit-datetime-edit-minute-field,
+ ::-webkit-datetime-edit-second-field,
+ ::-webkit-datetime-edit-millisecond-field,
+ ::-webkit-datetime-edit-meridiem-field {
+ padding-block: 0;
+ }
+ :-moz-ui-invalid {
+ box-shadow: none;
+ }
+ button,
+ input:where([type="button"], [type="reset"], [type="submit"]),
+ ::file-selector-button {
+ appearance: button;
+ }
+ ::-webkit-inner-spin-button,
+ ::-webkit-outer-spin-button {
+ height: auto;
+ }
+ [hidden]:where(:not([hidden="until-found"])) {
+ display: none !important;
+ }
+}
+@layer utilities {
+ .collapse {
+ visibility: collapse;
+ }
+ .invisible {
+ visibility: hidden;
+ }
+ .visible {
+ visibility: visible;
+ }
+ .absolute {
+ position: absolute;
+ }
+ .fixed {
+ position: fixed;
+ }
+ .relative {
+ position: relative;
+ }
+ .static {
+ position: static;
+ }
+ .top-12 {
+ top: calc(var(--spacing) * 12);
+ }
+ .top-full {
+ top: 100%;
+ }
+ .right-0 {
+ right: calc(var(--spacing) * 0);
+ }
+ .right-4 {
+ right: calc(var(--spacing) * 4);
+ }
+ .z-10 {
+ z-index: 10;
+ }
+ .z-\[100\] {
+ z-index: 100;
+ }
+ .col-span-full {
+ grid-column: 1 / -1;
+ }
+ .container {
+ width: 100%;
+ @media (width >= 40rem) {
+ max-width: 40rem;
+ }
+ @media (width >= 48rem) {
+ max-width: 48rem;
+ }
+ @media (width >= 64rem) {
+ max-width: 64rem;
+ }
+ @media (width >= 80rem) {
+ max-width: 80rem;
+ }
+ @media (width >= 96rem) {
+ max-width: 96rem;
+ }
+ }
+ .m-0 {
+ margin: calc(var(--spacing) * 0);
+ }
+ .mx-auto {
+ margin-inline: auto;
+ }
+ .mt-1\.5 {
+ margin-top: calc(var(--spacing) * 1.5);
+ }
+ .mt-2 {
+ margin-top: calc(var(--spacing) * 2);
+ }
+ .mt-2\.5 {
+ margin-top: calc(var(--spacing) * 2.5);
+ }
+ .mr-0 {
+ margin-right: calc(var(--spacing) * 0);
+ }
+ .mr-1 {
+ margin-right: calc(var(--spacing) * 1);
+ }
+ .mr-1\.5 {
+ margin-right: calc(var(--spacing) * 1.5);
+ }
+ .mr-2\.5 {
+ margin-right: calc(var(--spacing) * 2.5);
+ }
+ .mr-12 {
+ margin-right: calc(var(--spacing) * 12);
+ }
+ .mr-96 {
+ margin-right: calc(var(--spacing) * 96);
+ }
+ .mb-0 {
+ margin-bottom: calc(var(--spacing) * 0);
+ }
+ .mb-2 {
+ margin-bottom: calc(var(--spacing) * 2);
+ }
+ .ml-1 {
+ margin-left: calc(var(--spacing) * 1);
+ }
+ .ml-2 {
+ margin-left: calc(var(--spacing) * 2);
+ }
+ .block {
+ display: block;
+ }
+ .flex {
+ display: flex;
+ }
+ .grid {
+ display: grid;
+ }
+ .hidden {
+ display: none;
+ }
+ .inline-flex {
+ display: inline-flex;
+ }
+ .h-4 {
+ height: calc(var(--spacing) * 4);
+ }
+ .h-5 {
+ height: calc(var(--spacing) * 5);
+ }
+ .h-6 {
+ height: calc(var(--spacing) * 6);
+ }
+ .h-12 {
+ height: calc(var(--spacing) * 12);
+ }
+ .h-full {
+ height: 100%;
+ }
+ .h-screen {
+ height: 100vh;
+ }
+ .min-h-0 {
+ min-height: calc(var(--spacing) * 0);
+ }
+ .w-4 {
+ width: calc(var(--spacing) * 4);
+ }
+ .w-5 {
+ width: calc(var(--spacing) * 5);
+ }
+ .w-6 {
+ width: calc(var(--spacing) * 6);
+ }
+ .w-96 {
+ width: calc(var(--spacing) * 96);
+ }
+ .w-\[calc\(100\%-2\.5rem\)\] {
+ width: calc(100% - 2.5rem);
+ }
+ .w-\[calc\(100\%-24rem\)\] {
+ width: calc(100% - 24rem);
+ }
+ .w-full {
+ width: 100%;
+ }
+ .max-w-6xl {
+ max-width: var(--container-6xl);
+ }
+ .max-w-\[30\%\] {
+ max-width: 30%;
+ }
+ .max-w-full {
+ max-width: 100%;
+ }
+ .max-w-none {
+ max-width: none;
+ }
+ .min-w-24 {
+ min-width: calc(var(--spacing) * 24);
+ }
+ .min-w-96 {
+ min-width: calc(var(--spacing) * 96);
+ }
+ .flex-1 {
+ flex: 1;
+ }
+ .flex-shrink {
+ flex-shrink: 1;
+ }
+ .flex-shrink-0 {
+ flex-shrink: 0;
+ }
+ .flex-grow {
+ flex-grow: 1;
+ }
+ .transform {
+ transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,)
+ var(--tw-skew-x,) var(--tw-skew-y,);
+ }
+ .animate-pulse {
+ animation: var(--animate-pulse);
+ }
+ .cursor-default {
+ cursor: default;
+ }
+ .cursor-pointer {
+ cursor: pointer;
+ }
+ .resize {
+ resize: both;
+ }
+ .grid-cols-2 {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+ .grid-cols-\[repeat\(auto-fill\,minmax\(150px\,1fr\)\)\] {
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+ }
+ .flex-col {
+ flex-direction: column;
+ }
+ .flex-nowrap {
+ flex-wrap: nowrap;
+ }
+ .flex-wrap {
+ flex-wrap: wrap;
+ }
+ .items-center {
+ align-items: center;
+ }
+ .justify-between {
+ justify-content: space-between;
+ }
+ .justify-center {
+ justify-content: center;
+ }
+ .justify-start {
+ justify-content: flex-start;
+ }
+ .gap-0\.5 {
+ gap: calc(var(--spacing) * 0.5);
+ }
+ .gap-1\.5 {
+ gap: calc(var(--spacing) * 1.5);
+ }
+ .gap-2 {
+ gap: calc(var(--spacing) * 2);
+ }
+ .gap-2\.5 {
+ gap: calc(var(--spacing) * 2.5);
+ }
+ .gap-4 {
+ gap: calc(var(--spacing) * 4);
+ }
+ .gap-5 {
+ gap: calc(var(--spacing) * 5);
+ }
+ .self-end {
+ align-self: flex-end;
+ }
+ .self-stretch {
+ align-self: stretch;
+ }
+ .overflow-hidden {
+ overflow: hidden;
+ }
+ .overflow-x-hidden {
+ overflow-x: hidden;
+ }
+ .overflow-y-auto {
+ overflow-y: auto;
+ }
+ .rounded {
+ border-radius: 0.25rem;
+ }
+ .rounded-full {
+ border-radius: calc(infinity * 1px);
+ }
+ .rounded-lg {
+ border-radius: var(--radius-lg);
+ }
+ .border {
+ border-style: var(--tw-border-style);
+ border-width: 1px;
+ }
+ .border-t {
+ border-top-style: var(--tw-border-style);
+ border-top-width: 1px;
+ }
+ .border-r {
+ border-right-style: var(--tw-border-style);
+ border-right-width: 1px;
+ }
+ .border-b {
+ border-bottom-style: var(--tw-border-style);
+ border-bottom-width: 1px;
+ }
+ .border-b-2 {
+ border-bottom-style: var(--tw-border-style);
+ border-bottom-width: 2px;
+ }
+ .border-l {
+ border-left-style: var(--tw-border-style);
+ border-left-width: 1px;
+ }
+ .border-l-4 {
+ border-left-style: var(--tw-border-style);
+ border-left-width: 4px;
+ }
+ .border-none {
+ --tw-border-style: none;
+ border-style: none;
+ }
+ .border-blue-400 {
+ border-color: var(--color-blue-400);
+ }
+ .border-gray-200 {
+ border-color: var(--color-gray-200);
+ }
+ .border-gray-300 {
+ border-color: var(--color-gray-300);
+ }
+ .border-orange-500 {
+ border-color: var(--color-orange-500);
+ }
+ .border-transparent {
+ border-color: transparent;
+ }
+ .border-b-blue-500 {
+ border-bottom-color: var(--color-blue-500);
+ }
+ .bg-blue-50 {
+ background-color: var(--color-blue-50);
+ }
+ .bg-blue-100 {
+ background-color: var(--color-blue-100);
+ }
+ .bg-blue-500 {
+ background-color: var(--color-blue-500);
+ }
+ .bg-blue-700 {
+ background-color: var(--color-blue-700);
+ }
+ .bg-gray-50 {
+ background-color: var(--color-gray-50);
+ }
+ .bg-gray-100 {
+ background-color: var(--color-gray-100);
+ }
+ .bg-gray-600 {
+ background-color: var(--color-gray-600);
+ }
+ .bg-orange-50 {
+ background-color: var(--color-orange-50);
+ }
+ .bg-red-600 {
+ background-color: var(--color-red-600);
+ }
+ .bg-transparent {
+ background-color: transparent;
+ }
+ .bg-white {
+ background-color: var(--color-white);
+ }
+ .p-0 {
+ padding: calc(var(--spacing) * 0);
+ }
+ .px-1\.5 {
+ padding-inline: calc(var(--spacing) * 1.5);
+ }
+ .px-2 {
+ padding-inline: calc(var(--spacing) * 2);
+ }
+ .px-2\.5 {
+ padding-inline: calc(var(--spacing) * 2.5);
+ }
+ .px-3 {
+ padding-inline: calc(var(--spacing) * 3);
+ }
+ .px-4 {
+ padding-inline: calc(var(--spacing) * 4);
+ }
+ .px-5 {
+ padding-inline: calc(var(--spacing) * 5);
+ }
+ .py-0\.5 {
+ padding-block: calc(var(--spacing) * 0.5);
+ }
+ .py-1 {
+ padding-block: calc(var(--spacing) * 1);
+ }
+ .py-1\.5 {
+ padding-block: calc(var(--spacing) * 1.5);
+ }
+ .py-2 {
+ padding-block: calc(var(--spacing) * 2);
+ }
+ .py-2\.5 {
+ padding-block: calc(var(--spacing) * 2.5);
+ }
+ .pt-0 {
+ padding-top: calc(var(--spacing) * 0);
+ }
+ .pt-1\.5 {
+ padding-top: calc(var(--spacing) * 1.5);
+ }
+ .pt-2\.5 {
+ padding-top: calc(var(--spacing) * 2.5);
+ }
+ .pr-8 {
+ padding-right: calc(var(--spacing) * 8);
+ }
+ .pb-2\.5 {
+ padding-bottom: calc(var(--spacing) * 2.5);
+ }
+ .pl-4 {
+ padding-left: calc(var(--spacing) * 4);
+ }
+ .align-middle {
+ vertical-align: middle;
+ }
+ .font-mono {
+ font-family: var(--font-mono);
+ }
+ .font-sans {
+ font-family: var(--font-sans);
+ }
+ .text-base {
+ font-size: var(--text-base);
+ line-height: var(--tw-leading, var(--text-base--line-height));
+ }
+ .text-lg {
+ font-size: var(--text-lg);
+ line-height: var(--tw-leading, var(--text-lg--line-height));
+ }
+ .text-sm {
+ font-size: var(--text-sm);
+ line-height: var(--tw-leading, var(--text-sm--line-height));
+ }
+ .text-xs {
+ font-size: var(--text-xs);
+ line-height: var(--tw-leading, var(--text-xs--line-height));
+ }
+ .leading-relaxed {
+ --tw-leading: var(--leading-relaxed);
+ line-height: var(--leading-relaxed);
+ }
+ .font-bold {
+ --tw-font-weight: var(--font-weight-bold);
+ font-weight: var(--font-weight-bold);
+ }
+ .font-medium {
+ --tw-font-weight: var(--font-weight-medium);
+ font-weight: var(--font-weight-medium);
+ }
+ .font-normal {
+ --tw-font-weight: var(--font-weight-normal);
+ font-weight: var(--font-weight-normal);
+ }
+ .font-semibold {
+ --tw-font-weight: var(--font-weight-semibold);
+ font-weight: var(--font-weight-semibold);
+ }
+ .break-all {
+ word-break: break-all;
+ }
+ .text-ellipsis {
+ text-overflow: ellipsis;
+ }
+ .whitespace-nowrap {
+ white-space: nowrap;
+ }
+ .text-blue-500 {
+ color: var(--color-blue-500);
+ }
+ .text-blue-600 {
+ color: var(--color-blue-600);
+ }
+ .text-gray-400 {
+ color: var(--color-gray-400);
+ }
+ .text-gray-500 {
+ color: var(--color-gray-500);
+ }
+ .text-gray-600 {
+ color: var(--color-gray-600);
+ }
+ .text-gray-600\/85 {
+ color: color-mix(in srgb, oklch(44.6% 0.03 256.802) 85%, transparent);
+ @supports (color: color-mix(in lab, red, red)) {
+ color: color-mix(in oklab, var(--color-gray-600) 85%, transparent);
+ }
+ }
+ .text-gray-800 {
+ color: var(--color-gray-800);
+ }
+ .text-green-600 {
+ color: var(--color-green-600);
+ }
+ .text-orange-800 {
+ color: var(--color-orange-800);
+ }
+ .text-white {
+ color: var(--color-white);
+ }
+ .italic {
+ font-style: italic;
+ }
+ .no-underline {
+ text-decoration-line: none;
+ }
+ .opacity-70 {
+ opacity: 70%;
+ }
+ .shadow {
+ --tw-shadow:
+ 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)),
+ 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
+ box-shadow:
+ var(--tw-inset-shadow), var(--tw-inset-ring-shadow),
+ var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
+ }
+ .shadow-\[0_-2px_10px_rgba\(0\,0\,0\,0\.1\)\] {
+ --tw-shadow: 0 -2px 10px var(--tw-shadow-color, rgba(0, 0, 0, 0.1));
+ box-shadow:
+ var(--tw-inset-shadow), var(--tw-inset-ring-shadow),
+ var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
+ }
+ .shadow-lg {
+ --tw-shadow:
+ 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)),
+ 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
+ box-shadow:
+ var(--tw-inset-shadow), var(--tw-inset-ring-shadow),
+ var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
+ }
+ .shadow-sm {
+ --tw-shadow:
+ 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)),
+ 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
+ box-shadow:
+ var(--tw-inset-shadow), var(--tw-inset-ring-shadow),
+ var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
+ }
+ .outline {
+ outline-style: var(--tw-outline-style);
+ outline-width: 1px;
+ }
+ .blur {
+ --tw-blur: blur(8px);
+ filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,)
+ var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,)
+ var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
+ }
+ .backdrop-filter {
+ -webkit-backdrop-filter: var(--tw-backdrop-blur,)
+ var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,)
+ var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,)
+ var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,)
+ var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
+ backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,)
+ var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,)
+ var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,)
+ var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,)
+ var(--tw-backdrop-sepia,);
+ }
+ .transition {
+ transition-property:
+ color,
+ background-color,
+ border-color,
+ outline-color,
+ text-decoration-color,
+ fill,
+ stroke,
+ --tw-gradient-from,
+ --tw-gradient-via,
+ --tw-gradient-to,
+ opacity,
+ box-shadow,
+ transform,
+ translate,
+ scale,
+ rotate,
+ filter,
+ -webkit-backdrop-filter,
+ backdrop-filter,
+ display,
+ visibility,
+ content-visibility,
+ overlay,
+ pointer-events;
+ transition-timing-function: var(
+ --tw-ease,
+ var(--default-transition-timing-function)
+ );
+ transition-duration: var(--tw-duration, var(--default-transition-duration));
+ }
+ .transition-\[bottom\] {
+ transition-property: bottom;
+ transition-timing-function: var(
+ --tw-ease,
+ var(--default-transition-timing-function)
+ );
+ transition-duration: var(--tw-duration, var(--default-transition-duration));
+ }
+ .transition-\[margin-right\] {
+ transition-property: margin-right;
+ transition-timing-function: var(
+ --tw-ease,
+ var(--default-transition-timing-function)
+ );
+ transition-duration: var(--tw-duration, var(--default-transition-duration));
+ }
+ .transition-all {
+ transition-property: all;
+ transition-timing-function: var(
+ --tw-ease,
+ var(--default-transition-timing-function)
+ );
+ transition-duration: var(--tw-duration, var(--default-transition-duration));
+ }
+ .duration-200 {
+ --tw-duration: 200ms;
+ transition-duration: 200ms;
+ }
+ .ease-in-out {
+ --tw-ease: var(--ease-in-out);
+ transition-timing-function: var(--ease-in-out);
+ }
+ .group-hover\:opacity-100 {
+ &:is(:where(.group):hover *) {
+ @media (hover: hover) {
+ opacity: 100%;
+ }
+ }
+ }
+ .hover\:bg-blue-800 {
+ &:hover {
+ @media (hover: hover) {
+ background-color: var(--color-blue-800);
+ }
+ }
+ }
+ .hover\:bg-gray-200 {
+ &:hover {
+ @media (hover: hover) {
+ background-color: var(--color-gray-200);
+ }
+ }
+ }
+ .hover\:bg-gray-700 {
+ &:hover {
+ @media (hover: hover) {
+ background-color: var(--color-gray-700);
+ }
+ }
+ }
+ .hover\:bg-red-700 {
+ &:hover {
+ @media (hover: hover) {
+ background-color: var(--color-red-700);
+ }
+ }
+ }
+ .hover\:text-blue-600 {
+ &:hover {
+ @media (hover: hover) {
+ color: var(--color-blue-600);
+ }
+ }
+ }
+ .hover\:underline {
+ &:hover {
+ @media (hover: hover) {
+ text-decoration-line: underline;
+ }
+ }
+ }
+ .disabled\:cursor-not-allowed {
+ &:disabled {
+ cursor: not-allowed;
+ }
+ }
+ .disabled\:bg-gray-400 {
+ &:disabled {
+ background-color: var(--color-gray-400);
+ }
+ }
+ .disabled\:bg-red-300 {
+ &:disabled {
+ background-color: var(--color-red-300);
+ }
+ }
+ .disabled\:opacity-70 {
+ &:disabled {
+ opacity: 70%;
+ }
+ }
+ .disabled\:hover\:bg-red-300 {
+ &:disabled {
+ &:hover {
+ @media (hover: hover) {
+ background-color: var(--color-red-300);
+ }
+ }
+ }
+ }
+ .max-xl\:hidden {
+ @media (width < 80rem) {
+ display: none;
+ }
+ }
+ .max-xl\:px-1\.5 {
+ @media (width < 80rem) {
+ padding-inline: calc(var(--spacing) * 1.5);
+ }
+ }
+ .max-xl\:px-2\.5 {
+ @media (width < 80rem) {
+ padding-inline: calc(var(--spacing) * 2.5);
+ }
+ }
+ .max-xl\:py-1\.5 {
+ @media (width < 80rem) {
+ padding-block: calc(var(--spacing) * 1.5);
+ }
+ }
+ .max-md\:mr-0 {
+ @media (width < 48rem) {
+ margin-right: calc(var(--spacing) * 0);
+ }
+ }
+ .max-md\:hidden {
+ @media (width < 48rem) {
+ display: none;
+ }
+ }
+ .max-md\:w-full {
+ @media (width < 48rem) {
+ width: 100%;
+ }
+ }
+ .md\:mr-72 {
+ @media (width >= 48rem) {
+ margin-right: calc(var(--spacing) * 72);
+ }
+ }
+ .md\:w-72 {
+ @media (width >= 48rem) {
+ width: calc(var(--spacing) * 72);
+ }
+ }
+ .md\:w-\[calc\(100\%-18rem\)\] {
+ @media (width >= 48rem) {
+ width: calc(100% - 18rem);
+ }
+ }
+ .lg\:mr-80 {
+ @media (width >= 64rem) {
+ margin-right: calc(var(--spacing) * 80);
+ }
+ }
+ .lg\:w-80 {
+ @media (width >= 64rem) {
+ width: calc(var(--spacing) * 80);
+ }
+ }
+ .lg\:w-\[calc\(100\%-20rem\)\] {
+ @media (width >= 64rem) {
+ width: calc(100% - 20rem);
+ }
+ }
+ .xl\:mr-96 {
+ @media (width >= 80rem) {
+ margin-right: calc(var(--spacing) * 96);
+ }
+ }
+ .xl\:inline {
+ @media (width >= 80rem) {
+ display: inline;
+ }
+ }
+ .xl\:w-96 {
+ @media (width >= 80rem) {
+ width: calc(var(--spacing) * 96);
+ }
+ }
+ .xl\:w-\[calc\(100\%-24rem\)\] {
+ @media (width >= 80rem) {
+ width: calc(100% - 24rem);
+ }
+ }
+ .xl\:px-2\.5 {
+ @media (width >= 80rem) {
+ padding-inline: calc(var(--spacing) * 2.5);
+ }
+ }
+ .xl\:py-1 {
+ @media (width >= 80rem) {
+ padding-block: calc(var(--spacing) * 1);
+ }
+ }
+}
+@property --tw-rotate-x {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-rotate-y {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-rotate-z {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-skew-x {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-skew-y {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-border-style {
+ syntax: "*";
+ inherits: false;
+ initial-value: solid;
+}
+@property --tw-leading {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-font-weight {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-shadow {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0 0 #0000;
+}
+@property --tw-shadow-color {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-shadow-alpha {
+ syntax: "<percentage>";
+ inherits: false;
+ initial-value: 100%;
+}
+@property --tw-inset-shadow {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0 0 #0000;
+}
+@property --tw-inset-shadow-color {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-inset-shadow-alpha {
+ syntax: "<percentage>";
+ inherits: false;
+ initial-value: 100%;
+}
+@property --tw-ring-color {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-ring-shadow {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0 0 #0000;
+}
+@property --tw-inset-ring-color {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-inset-ring-shadow {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0 0 #0000;
+}
+@property --tw-ring-inset {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-ring-offset-width {
+ syntax: "<length>";
+ inherits: false;
+ initial-value: 0px;
+}
+@property --tw-ring-offset-color {
+ syntax: "*";
+ inherits: false;
+ initial-value: #fff;
+}
+@property --tw-ring-offset-shadow {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0 0 #0000;
+}
+@property --tw-outline-style {
+ syntax: "*";
+ inherits: false;
+ initial-value: solid;
+}
+@property --tw-blur {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-brightness {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-contrast {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-grayscale {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-hue-rotate {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-invert {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-opacity {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-saturate {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-sepia {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-drop-shadow {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-drop-shadow-color {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-drop-shadow-alpha {
+ syntax: "<percentage>";
+ inherits: false;
+ initial-value: 100%;
+}
+@property --tw-drop-shadow-size {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-backdrop-blur {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-backdrop-brightness {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-backdrop-contrast {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-backdrop-grayscale {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-backdrop-hue-rotate {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-backdrop-invert {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-backdrop-opacity {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-backdrop-saturate {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-backdrop-sepia {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-duration {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-ease {
+ syntax: "*";
+ inherits: false;
+}
+@keyframes pulse {
+ 50% {
+ opacity: 0.5;
+ }
+}
+@layer properties {
+ @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or
+ ((-moz-orient: inline) and (not (color: rgb(from red r g b)))) {
+ *,
+ ::before,
+ ::after,
+ ::backdrop {
+ --tw-rotate-x: initial;
+ --tw-rotate-y: initial;
+ --tw-rotate-z: initial;
+ --tw-skew-x: initial;
+ --tw-skew-y: initial;
+ --tw-border-style: solid;
+ --tw-leading: initial;
+ --tw-font-weight: initial;
+ --tw-shadow: 0 0 #0000;
+ --tw-shadow-color: initial;
+ --tw-shadow-alpha: 100%;
+ --tw-inset-shadow: 0 0 #0000;
+ --tw-inset-shadow-color: initial;
+ --tw-inset-shadow-alpha: 100%;
+ --tw-ring-color: initial;
+ --tw-ring-shadow: 0 0 #0000;
+ --tw-inset-ring-color: initial;
+ --tw-inset-ring-shadow: 0 0 #0000;
+ --tw-ring-inset: initial;
+ --tw-ring-offset-width: 0px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-offset-shadow: 0 0 #0000;
+ --tw-outline-style: solid;
+ --tw-blur: initial;
+ --tw-brightness: initial;
+ --tw-contrast: initial;
+ --tw-grayscale: initial;
+ --tw-hue-rotate: initial;
+ --tw-invert: initial;
+ --tw-opacity: initial;
+ --tw-saturate: initial;
+ --tw-sepia: initial;
+ --tw-drop-shadow: initial;
+ --tw-drop-shadow-color: initial;
+ --tw-drop-shadow-alpha: 100%;
+ --tw-drop-shadow-size: initial;
+ --tw-backdrop-blur: initial;
+ --tw-backdrop-brightness: initial;
+ --tw-backdrop-contrast: initial;
+ --tw-backdrop-grayscale: initial;
+ --tw-backdrop-hue-rotate: initial;
+ --tw-backdrop-invert: initial;
+ --tw-backdrop-opacity: initial;
+ --tw-backdrop-saturate: initial;
+ --tw-backdrop-sepia: initial;
+ --tw-duration: initial;
+ --tw-ease: initial;
+ }
+ }
+}
diff --git a/webui/src/web-components/sketch-app-shell.ts b/webui/src/web-components/sketch-app-shell.ts
index 7f51576..f37d9c9 100644
--- a/webui/src/web-components/sketch-app-shell.ts
+++ b/webui/src/web-components/sketch-app-shell.ts
@@ -3,6 +3,7 @@
import { ConnectionStatus, DataManager } from "../data";
import { AgentMessage, GitLogEntry, State } from "../types";
import { aggregateAgentMessages } from "./aggregateAgentMessages";
+import { SketchTailwindElement } from "./sketch-tailwind-element";
import "./sketch-chat-input";
import "./sketch-container-status";
@@ -24,7 +25,7 @@
type ViewMode = "chat" | "diff2" | "terminal";
@customElement("sketch-app-shell")
-export class SketchAppShell extends LitElement {
+export class SketchAppShell extends SketchTailwindElement {
// Current view mode (chat, diff, terminal)
@state()
viewMode: ViewMode = "chat";
@@ -39,512 +40,32 @@
// Reference to the container status element
containerStatusElement: any = null;
- // See https://lit.dev/docs/components/styles/ for how lit-element handles CSS.
- // Note that these styles only apply to the scope of this web component's
- // shadow DOM node, so they won't leak out or collide with CSS declared in
- // other components or the containing web page (...unless you want it to do that).
- static styles = css`
- .copied-indicator {
- position: absolute;
- top: -20px;
- left: 50%;
- transform: translateX(-50%);
- background: rgba(40, 167, 69, 0.9);
- color: white;
- padding: 2px 6px;
- border-radius: 3px;
- font-size: 10px;
- font-family: system-ui, sans-serif;
- animation: fadeInOut 2s ease;
- pointer-events: none;
- }
+ // Note: CSS styles have been converted to Tailwind classes applied directly to HTML elements
+ // since this component now extends SketchTailwindElement which disables shadow DOM
- @keyframes fadeInOut {
- 0% {
- opacity: 0;
- }
- 20% {
- opacity: 1;
- }
- 80% {
- opacity: 1;
- }
- 100% {
- opacity: 0;
- }
- }
-
- .commit-branch-indicator {
- color: #28a745;
- }
-
- .commit-hash-indicator {
- color: #0366d6;
- }
- :host {
- display: block;
- font-family:
- system-ui,
- -apple-system,
- BlinkMacSystemFont,
- "Segoe UI",
- Roboto,
- sans-serif;
- color: #333;
- line-height: 1.4;
- height: 100vh;
- width: 100%;
- position: relative;
- overflow-x: hidden;
- display: flex;
- flex-direction: column;
- }
-
- /* Top banner with combined elements */
- #top-banner {
- display: flex;
- align-self: stretch;
- justify-content: space-between;
- align-items: center;
- padding: 0 20px;
- margin-bottom: 0;
- border-bottom: 1px solid #eee;
- gap: 20px;
- background: white;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- width: 100%;
- height: 48px;
- padding-right: 30px; /* Extra padding on the right to prevent elements from hitting the edge */
- }
-
- /* View mode container styles - mirroring timeline.css structure */
- #view-container {
- align-self: stretch;
- overflow-y: auto;
- flex: 1;
- display: flex;
- flex-direction: column;
- min-height: 0; /* Critical for proper flex child behavior */
- }
-
- #view-container-inner {
- max-width: 1200px;
- width: calc(100% - 40px);
- margin: 0 auto;
- position: relative;
- padding-bottom: 10px;
- padding-top: 10px;
- display: flex;
- flex-direction: column;
- height: 100%; /* Ensure it takes full height of parent */
- }
-
- /* Adjust view container when todo panel is visible in chat mode */
- #view-container-inner.with-todo-panel {
- max-width: none;
- width: 100%;
- margin: 0;
- padding-left: 20px;
- padding-right: 20px;
- }
-
- #chat-input {
- align-self: flex-end;
- width: 100%;
- box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
- }
-
- .banner-title {
- font-size: 18px;
- font-weight: 600;
- margin: 0;
- min-width: 6em;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- .banner-title a {
- color: inherit;
- text-decoration: none;
- transition: opacity 0.2s ease;
- display: flex;
- align-items: center;
- gap: 8px;
- }
-
- .banner-title a:hover {
- opacity: 0.8;
- text-decoration: underline;
- }
-
- .banner-title img {
- width: 20px;
- height: 20px;
- border-radius: 3px;
- }
-
- /* Mobile-specific styles for banner link */
- @media (max-width: 768px) {
- .title-container {
- max-width: 50%; /* Allow more space on mobile */
- }
-
- .banner-title {
- font-size: 16px; /* Slightly smaller on mobile */
- }
-
- .banner-title img {
- width: 18px;
- height: 18px;
- }
- }
-
- @media (max-width: 480px) {
- .title-container {
- max-width: 60%; /* Even more space on very small screens */
- }
-
- .banner-title {
- font-size: 14px;
- }
-
- .banner-title img {
- width: 16px;
- height: 16px;
- }
- }
-
- .slug-title {
- margin: 0;
- padding: 0;
- color: rgba(82, 82, 82, 0.85);
- font-size: 14px;
- font-weight: normal;
- font-style: italic;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- /* Allow the container to expand to full width and height in diff mode */
- #view-container-inner.diff2-active {
- max-width: 100%;
- width: 100%;
- height: 100%;
- padding: 0; /* Remove padding for more space */
- display: flex;
- flex-direction: column;
- flex: 1;
- min-height: 0; /* Critical for flex behavior */
- }
-
- /* Individual view styles */
- .chat-view,
- .diff2-view,
- .terminal-view {
- display: none; /* Hidden by default */
- width: 100%;
- height: 100%;
- }
-
- /* Make chat view take full width available */
- .chat-view.view-active {
- display: flex;
- flex-direction: column;
- width: 100%;
- height: 100%;
- }
-
- /* Chat timeline container - takes full width, memory panel will be positioned separately */
- .chat-timeline-container {
- flex: 1;
- display: flex;
- flex-direction: column;
- width: 100%;
- height: 100%;
- margin-right: 0; /* Default - no memory panel */
- transition: margin-right 0.2s ease; /* Smooth transition */
- }
-
- /* Adjust chat timeline container when todo panel is visible */
- .chat-timeline-container.with-todo-panel {
- margin-right: 400px; /* Make space for fixed todo panel */
- width: calc(100% - 400px); /* Explicitly set width to prevent overlap */
- }
-
- /* Todo panel container - fixed to right side */
- .todo-panel-container {
- position: fixed;
- top: 48px; /* Below top banner */
- right: 15px; /* Leave space for scroll bar */
- width: 400px;
- bottom: var(
- --chat-input-height,
- 90px
- ); /* Dynamic height based on chat input size */
- background-color: #fafafa;
- border-left: 1px solid #e0e0e0;
- z-index: 100;
- display: none; /* Hidden by default */
- transition: bottom 0.2s ease; /* Smooth transition when height changes */
- /* Add fuzzy gradient at bottom to blend with text entry */
- background: linear-gradient(
- to bottom,
- #fafafa 0%,
- #fafafa 90%,
- rgba(250, 250, 250, 0.5) 95%,
- rgba(250, 250, 250, 0.2) 100%
- );
- }
-
- .todo-panel-container.visible {
- display: block;
- }
-
- /* Responsive adjustments for todo panel */
- @media (max-width: 1200px) {
- .todo-panel-container {
- width: 350px;
- /* bottom is still controlled by --chat-input-height CSS variable */
- }
- .chat-timeline-container.with-todo-panel {
- margin-right: 350px;
- width: calc(100% - 350px);
- }
- }
-
- @media (max-width: 900px) {
- .todo-panel-container {
- width: 300px;
- /* bottom is still controlled by --chat-input-height CSS variable */
- }
- .chat-timeline-container.with-todo-panel {
- margin-right: 300px;
- width: calc(100% - 300px);
- }
- }
-
- /* On very small screens, hide todo panel or make it overlay */
- @media (max-width: 768px) {
- .todo-panel-container.visible {
- display: none; /* Hide on mobile */
- }
- .chat-timeline-container.with-todo-panel {
- margin-right: 0;
+ // Override createRenderRoot to apply host styles for proper sizing while still using light DOM
+ createRenderRoot() {
+ // Use light DOM like SketchTailwindElement but still apply host styles
+ const style = document.createElement("style");
+ style.textContent = `
+ sketch-app-shell {
+ display: block;
width: 100%;
+ height: 100vh;
+ max-width: 100%;
+ box-sizing: border-box;
+ overflow: hidden;
}
+ `;
+
+ // Add the style to the document head if not already present
+ if (!document.head.querySelector("style[data-sketch-app-shell]")) {
+ style.setAttribute("data-sketch-app-shell", "");
+ document.head.appendChild(style);
}
- /* Monaco diff2 view needs to take all available space */
- .diff2-view.view-active {
- flex: 1;
- overflow: hidden;
- min-height: 0; /* Required for proper flex child behavior */
- display: flex;
- flex-direction: column;
- height: 100%;
- }
-
- /* Active view styles - these will be applied via JavaScript */
- .view-active {
- display: flex;
- flex-direction: column;
- }
-
- .title-container {
- display: flex;
- flex-direction: column;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 30%;
- padding: 6px 0;
- }
-
- .refresh-control {
- display: flex;
- align-items: center;
- margin-bottom: 0;
- flex-wrap: nowrap;
- white-space: nowrap;
- flex-shrink: 0;
- gap: 15px;
- padding-left: 15px;
- margin-right: 50px;
- }
-
- .stop-button,
- .end-button {
- background: #2196f3;
- color: white;
- border: none;
- padding: 4px 10px;
- border-radius: 4px;
- cursor: pointer;
- font-size: 12px;
- margin-right: 5px;
- display: flex;
- align-items: center;
- gap: 6px;
- }
-
- .stop-button {
- background: #dc3545;
- color: white;
- }
-
- .stop-button:hover:not(:disabled) {
- background-color: #c82333;
- }
-
- .stop-button:disabled {
- background-color: #e9a8ad;
- cursor: not-allowed;
- opacity: 0.7;
- }
-
- .stop-button:disabled:hover {
- background-color: #e9a8ad;
- }
-
- .end-button {
- background: #6c757d;
- color: white;
- }
-
- .end-button:hover:not(:disabled) {
- background-color: #5a6268;
- }
-
- .end-button:disabled {
- background-color: #a9acaf;
- cursor: not-allowed;
- opacity: 0.7;
- }
-
- .button-icon {
- width: 16px;
- height: 16px;
- }
-
- @media (max-width: 1400px) {
- .button-text {
- display: none;
- }
-
- .stop-button {
- padding: 6px;
- }
- }
-
- /* Removed poll-updates class */
-
- .notifications-toggle {
- display: flex;
- align-items: center;
- font-size: 12px;
- margin-right: 10px;
- cursor: pointer;
- }
-
- .bell-icon {
- width: 20px;
- height: 20px;
- position: relative;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- }
-
- .bell-disabled::before {
- content: "";
- position: absolute;
- width: 2px;
- height: 24px;
- background-color: #dc3545;
- transform: rotate(45deg);
- transform-origin: center center;
- }
-
- /* Print styles for full chat printing */
- @media print {
- :host {
- height: auto !important;
- overflow: visible !important;
- display: block !important;
- }
-
- /* Hide non-essential UI elements during printing */
- #top-banner {
- border-bottom: 1px solid #000;
- box-shadow: none;
- page-break-inside: avoid;
- }
-
- /* Hide interactive elements that aren't useful in print */
- .stop-button,
- .end-button,
- .notifications-toggle,
- sketch-call-status,
- sketch-network-status,
- sketch-view-mode-select {
- display: none !important;
- }
-
- /* Ensure view container can expand to full content */
- #view-container {
- overflow: visible !important;
- flex: none !important;
- height: auto !important;
- max-height: none !important;
- }
-
- #view-container-inner {
- height: auto !important;
- max-height: none !important;
- overflow: visible !important;
- }
-
- /* Remove todo panel from print to avoid layout issues */
- .todo-panel-container {
- display: none !important;
- }
-
- /* Ensure chat timeline container takes full width in print */
- .chat-timeline-container {
- margin-right: 0 !important;
- width: 100% !important;
- height: auto !important;
- overflow: visible !important;
- }
-
- /* Make chat view fully visible */
- .chat-view {
- height: auto !important;
- overflow: visible !important;
- }
-
- /* Hide chat input during printing */
- #chat-input {
- display: none !important;
- }
-
- /* Adjust diff2 and terminal views for print */
- .diff2-view,
- .terminal-view {
- height: auto !important;
- overflow: visible !important;
- }
-
- /* Ensure only active view is shown in print */
- .chat-view:not(.view-active),
- .diff2-view:not(.view-active),
- .terminal-view:not(.view-active) {
- display: none !important;
- }
- }
- `;
+ return this;
+ }
// Header bar: Network connection status details
@property()
@@ -859,68 +380,39 @@
// Wait for DOM update to complete
this.updateComplete.then(() => {
- // Update active view
- const viewContainerInner = this.shadowRoot?.querySelector(
- "#view-container-inner",
- );
- const chatView = this.shadowRoot?.querySelector(".chat-view");
- const diff2View = this.shadowRoot?.querySelector(".diff2-view");
- const terminalView = this.shadowRoot?.querySelector(".terminal-view");
-
- // Remove active class from all views
- chatView?.classList.remove("view-active");
- diff2View?.classList.remove("view-active");
- terminalView?.classList.remove("view-active");
-
- // Add/remove diff2-active class on view container
- if (mode === "diff2") {
- viewContainerInner?.classList.add("diff2-active");
- } else {
- viewContainerInner?.classList.remove("diff2-active");
+ // Handle scroll position restoration for chat view
+ if (
+ mode === "chat" &&
+ this.scrollContainerRef.value &&
+ this._chatScrollPosition > 0
+ ) {
+ // Use requestAnimationFrame to ensure DOM is ready
+ requestAnimationFrame(() => {
+ if (this.scrollContainerRef.value) {
+ // Double-check that we're still in chat mode and the container is available
+ if (
+ this.viewMode === "chat" &&
+ this.scrollContainerRef.value.isConnected
+ ) {
+ this.scrollContainerRef.value.scrollTop =
+ this._chatScrollPosition;
+ }
+ }
+ });
}
- // Add active class to the selected view
- switch (mode) {
- case "chat":
- chatView?.classList.add("view-active");
- // Restore scroll position if we're switching back to chat
- if (this.scrollContainerRef.value && this._chatScrollPosition > 0) {
- // Use requestAnimationFrame to ensure DOM is ready
- requestAnimationFrame(() => {
- if (this.scrollContainerRef.value) {
- // Double-check that we're still in chat mode and the container is available
- if (
- this.viewMode === "chat" &&
- this.scrollContainerRef.value.isConnected
- ) {
- this.scrollContainerRef.value.scrollTop =
- this._chatScrollPosition;
- }
- }
- });
- }
- break;
-
- case "diff2":
- diff2View?.classList.add("view-active");
- // Refresh git/recentlog when Monaco diff view is opened
- // This ensures branch information is always up-to-date, as branches can change frequently
- const diff2ViewComp =
- this.shadowRoot?.querySelector("sketch-diff2-view");
- if (diff2ViewComp) {
- (diff2ViewComp as SketchDiff2View).refreshDiffView();
- }
- break;
-
- case "terminal":
- terminalView?.classList.add("view-active");
- break;
+ // Handle diff2 view specific logic
+ if (mode === "diff2") {
+ // Refresh git/recentlog when Monaco diff view is opened
+ // This ensures branch information is always up-to-date, as branches can change frequently
+ const diff2ViewComp = this.querySelector("sketch-diff2-view");
+ if (diff2ViewComp) {
+ (diff2ViewComp as SketchDiff2View).refreshDiffView();
+ }
}
// Update view mode buttons
- const viewModeSelect = this.shadowRoot?.querySelector(
- "sketch-view-mode-select",
- );
+ const viewModeSelect = this.querySelector("sketch-view-mode-select");
if (viewModeSelect) {
const event = new CustomEvent("update-active-mode", {
detail: { mode },
@@ -1352,194 +844,239 @@
render() {
return html`
- <div id="top-banner">
- <div class="title-container">
- <h1 class="banner-title">
- ${this.containerState?.skaband_addr
- ? html`<a
- href="${this.containerState.skaband_addr}"
- target="_blank"
- rel="noopener noreferrer"
- >
- <img
- src="${this.containerState.skaband_addr}/sketch.dev.png"
- alt="sketch"
- />
- sketch
- </a>`
- : html`sketch`}
- </h1>
- <h2 class="slug-title">${this.slug}</h2>
- </div>
-
- <!-- Container status info moved above tabs -->
- <sketch-container-status
- .state=${this.containerState}
- id="container-status"
- ></sketch-container-status>
-
- <!-- Last Commit section moved to sketch-container-status -->
-
- <!-- Views section with tabs -->
- <sketch-view-mode-select
- .diffLinesAdded=${this.containerState?.diff_lines_added || 0}
- .diffLinesRemoved=${this.containerState?.diff_lines_removed || 0}
- ></sketch-view-mode-select>
-
- <div class="refresh-control">
- <button
- id="stopButton"
- class="stop-button"
- ?disabled=${(this.containerState?.outstanding_llm_calls || 0) ===
- 0 &&
- (this.containerState?.outstanding_tool_calls || []).length === 0}
- >
- <svg
- class="button-icon"
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- stroke-width="2"
- stroke-linecap="round"
- stroke-linejoin="round"
- >
- <rect x="6" y="6" width="12" height="12" />
- </svg>
- <span class="button-text">Stop</span>
- </button>
- <button
- id="endButton"
- class="end-button"
- @click=${this._handleEndClick}
- >
- <svg
- class="button-icon"
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- stroke-width="2"
- stroke-linecap="round"
- stroke-linejoin="round"
- >
- <path d="M18 6L6 18" />
- <path d="M6 6l12 12" />
- </svg>
- <span class="button-text">End</span>
- </button>
-
+ <!-- Main container: flex column, full height, system font, hidden overflow-x -->
+ <div
+ class="block font-sans text-gray-800 leading-relaxed h-screen w-full relative overflow-x-hidden flex flex-col"
+ >
+ <!-- Top banner: flex row, space between, border bottom, shadow -->
+ <div
+ id="top-banner"
+ class="flex self-stretch justify-between items-center px-5 pr-8 mb-0 border-b border-gray-200 gap-5 bg-white shadow-md w-full h-12"
+ >
+ <!-- Title container -->
<div
- class="notifications-toggle"
- @click=${this._handleNotificationsToggle}
- title="${this.notificationsEnabled
- ? "Disable"
- : "Enable"} notifications when the agent completes its turn"
+ class="flex flex-col whitespace-nowrap overflow-hidden text-ellipsis max-w-[30%] md:max-w-1/2 sm:max-w-[60%] py-1.5"
>
- <div
- class="bell-icon ${!this.notificationsEnabled
- ? "bell-disabled"
- : ""}"
+ <h1
+ class="text-lg md:text-base sm:text-sm font-semibold m-0 min-w-24 whitespace-nowrap overflow-hidden text-ellipsis"
>
- <!-- Bell SVG icon -->
- <svg
- xmlns="http://www.w3.org/2000/svg"
- width="16"
- height="16"
- fill="currentColor"
- viewBox="0 0 16 16"
- >
- <path
- d="M8 16a2 2 0 0 0 2-2H6a2 2 0 0 0 2 2zM8 1.918l-.797.161A4.002 4.002 0 0 0 4 6c0 .628-.134 2.197-.459 3.742-.16.767-.376 1.566-.663 2.258h10.244c-.287-.692-.502-1.49-.663-2.258C12.134 8.197 12 6.628 12 6a4.002 4.002 0 0 0-3.203-3.92L8 1.917zM14.22 12c.223.447.481.801.78 1H1c.299-.199.557-.553.78-1C2.68 10.2 3 6.88 3 6c0-2.42 1.72-4.44 4.005-4.901a1 1 0 1 1 1.99 0A5.002 5.002 0 0 1 13 6c0 .88.32 4.2 1.22 6z"
- />
- </svg>
- </div>
+ ${this.containerState?.skaband_addr
+ ? html`<a
+ href="${this.containerState.skaband_addr}"
+ target="_blank"
+ rel="noopener noreferrer"
+ class="text-inherit no-underline transition-opacity duration-200 ease-in-out flex items-center gap-2 hover:opacity-80 hover:underline"
+ >
+ <img
+ src="${this.containerState.skaband_addr}/sketch.dev.png"
+ alt="sketch"
+ class="w-5 h-5 md:w-[18px] md:h-[18px] sm:w-4 sm:h-4 rounded-sm"
+ />
+ sketch
+ </a>`
+ : html`sketch`}
+ </h1>
+ <h2
+ class="m-0 p-0 text-gray-600 text-sm font-normal italic whitespace-nowrap overflow-hidden text-ellipsis"
+ >
+ ${this.slug}
+ </h2>
</div>
- <sketch-call-status
- .agentState=${this.containerState?.agent_state}
- .llmCalls=${this.containerState?.outstanding_llm_calls || 0}
- .toolCalls=${this.containerState?.outstanding_tool_calls || []}
- .isIdle=${(() => {
- const lastUserOrAgentMessage = this.getLastUserOrAgentMessage();
- return lastUserOrAgentMessage
- ? lastUserOrAgentMessage.end_of_turn &&
- !lastUserOrAgentMessage.parent_conversation_id
- : true;
- })()}
- .isDisconnected=${this.connectionStatus === "disconnected"}
- ></sketch-call-status>
+ <!-- Container status info moved above tabs -->
+ <sketch-container-status
+ .state=${this.containerState}
+ id="container-status"
+ ></sketch-container-status>
- <sketch-network-status
- connection=${this.connectionStatus}
- error=${this.connectionErrorMessage}
- ></sketch-network-status>
+ <!-- Last Commit section moved to sketch-container-status -->
+
+ <!-- Views section with tabs -->
+ <sketch-view-mode-select
+ .diffLinesAdded=${this.containerState?.diff_lines_added || 0}
+ .diffLinesRemoved=${this.containerState?.diff_lines_removed || 0}
+ ></sketch-view-mode-select>
+
+ <!-- Control buttons and status -->
+ <div
+ class="flex items-center mb-0 flex-nowrap whitespace-nowrap flex-shrink-0 gap-4 pl-4 mr-12"
+ >
+ <button
+ id="stopButton"
+ class="bg-red-600 hover:bg-red-700 disabled:bg-red-300 disabled:cursor-not-allowed disabled:opacity-70 text-white border-none px-2.5 py-1 xl:px-1.5 rounded cursor-pointer text-xs mr-1.5 flex items-center gap-1.5 transition-colors"
+ ?disabled=${(this.containerState?.outstanding_llm_calls || 0) ===
+ 0 &&
+ (this.containerState?.outstanding_tool_calls || []).length === 0}
+ >
+ <svg
+ class="w-4 h-4"
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ stroke-width="2"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ >
+ <rect x="6" y="6" width="12" height="12" />
+ </svg>
+ <span class="xl:hidden">Stop</span>
+ </button>
+ <button
+ id="endButton"
+ class="bg-gray-600 hover:bg-gray-700 disabled:bg-gray-400 disabled:cursor-not-allowed disabled:opacity-70 text-white border-none px-2.5 py-1 xl:px-1.5 rounded cursor-pointer text-xs mr-1.5 flex items-center gap-1.5 transition-colors"
+ @click=${this._handleEndClick}
+ >
+ <svg
+ class="w-4 h-4"
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ stroke-width="2"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ >
+ <path d="M18 6L6 18" />
+ <path d="M6 6l12 12" />
+ </svg>
+ <span class="xl:hidden">End</span>
+ </button>
+
+ <div
+ class="flex items-center text-xs mr-2.5 cursor-pointer"
+ @click=${this._handleNotificationsToggle}
+ title="${this.notificationsEnabled
+ ? "Disable"
+ : "Enable"} notifications when the agent completes its turn"
+ >
+ <div
+ class="w-5 h-5 relative inline-flex items-center justify-center"
+ >
+ <!-- Bell SVG icon -->
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="16"
+ height="16"
+ fill="currentColor"
+ viewBox="0 0 16 16"
+ class="${!this.notificationsEnabled ? "relative z-10" : ""}"
+ >
+ <path
+ d="M8 16a2 2 0 0 0 2-2H6a2 2 0 0 0 2 2zM8 1.918l-.797.161A4.002 4.002 0 0 0 4 6c0 .628-.134 2.197-.459 3.742-.16.767-.376 1.566-.663 2.258h10.244c-.287-.692-.502-1.49-.663-2.258C12.134 8.197 12 6.628 12 6a4.002 4.002 0 0 0-3.203-3.92L8 1.917zM14.22 12c.223.447.481.801.78 1H1c.299-.199.557-.553.78-1C2.68 10.2 3 6.88 3 6c0-2.42 1.72-4.44 4.005-4.901a1 1 0 1 1 1.99 0A5.002 5.002 0 0 1 13 6c0 .88.32 4.2 1.22 6z"
+ />
+ </svg>
+ ${!this.notificationsEnabled
+ ? html`<div
+ class="absolute w-0.5 h-6 bg-red-600 rotate-45 origin-center"
+ ></div>`
+ : ""}
+ </div>
+ </div>
+
+ <sketch-call-status
+ .agentState=${this.containerState?.agent_state}
+ .llmCalls=${this.containerState?.outstanding_llm_calls || 0}
+ .toolCalls=${this.containerState?.outstanding_tool_calls || []}
+ .isIdle=${(() => {
+ const lastUserOrAgentMessage = this.getLastUserOrAgentMessage();
+ return lastUserOrAgentMessage
+ ? lastUserOrAgentMessage.end_of_turn &&
+ !lastUserOrAgentMessage.parent_conversation_id
+ : true;
+ })()}
+ .isDisconnected=${this.connectionStatus === "disconnected"}
+ ></sketch-call-status>
+
+ <sketch-network-status
+ connection=${this.connectionStatus}
+ error=${this.connectionErrorMessage}
+ ></sketch-network-status>
+ </div>
</div>
- </div>
- <div id="view-container" ${ref(this.scrollContainerRef)}>
+ <!-- Main content area: scrollable, flex-1 -->
<div
- id="view-container-inner"
- class="${this._todoPanelVisible && this.viewMode === "chat"
- ? "with-todo-panel"
- : ""}"
+ id="view-container"
+ ${ref(this.scrollContainerRef)}
+ class="self-stretch overflow-y-auto flex-1 flex flex-col min-h-0"
>
<div
- class="chat-view ${this.viewMode === "chat" ? "view-active" : ""}"
+ id="view-container-inner"
+ class="${this.viewMode === "diff2"
+ ? "max-w-full w-full h-full p-0 flex flex-col flex-1 min-h-0"
+ : this._todoPanelVisible && this.viewMode === "chat"
+ ? "max-w-none w-full m-0 px-5"
+ : "max-w-6xl w-[calc(100%-40px)] mx-auto"} relative pb-2.5 pt-2.5 flex flex-col h-full"
>
+ <!-- Chat View -->
<div
- class="chat-timeline-container ${this._todoPanelVisible &&
- this.viewMode === "chat"
- ? "with-todo-panel"
- : ""}"
+ class="chat-view ${this.viewMode === "chat"
+ ? "view-active flex flex-col"
+ : "hidden"} w-full h-full"
>
- <sketch-timeline
- .messages=${this.messages}
- .scrollContainer=${this.scrollContainerRef}
- .agentState=${this.containerState?.agent_state}
- .llmCalls=${this.containerState?.outstanding_llm_calls || 0}
- .toolCalls=${this.containerState?.outstanding_tool_calls || []}
- .firstMessageIndex=${this.containerState?.first_message_index ||
- 0}
- .state=${this.containerState}
- .dataManager=${this.dataManager}
- ></sketch-timeline>
+ <div
+ class="${this._todoPanelVisible && this.viewMode === "chat"
+ ? "mr-[400px] xl:mr-[350px] lg:mr-[300px] md:mr-0 w-[calc(100%-400px)] xl:w-[calc(100%-350px)] lg:w-[calc(100%-300px)] md:w-full"
+ : "mr-0"} flex-1 flex flex-col w-full h-full transition-[margin-right] duration-200 ease-in-out"
+ >
+ <sketch-timeline
+ .messages=${this.messages}
+ .scrollContainer=${this.scrollContainerRef}
+ .agentState=${this.containerState?.agent_state}
+ .llmCalls=${this.containerState?.outstanding_llm_calls || 0}
+ .toolCalls=${this.containerState?.outstanding_tool_calls ||
+ []}
+ .firstMessageIndex=${this.containerState
+ ?.first_message_index || 0}
+ .state=${this.containerState}
+ .dataManager=${this.dataManager}
+ ></sketch-timeline>
+ </div>
+ </div>
+
+ <!-- Todo panel positioned outside the main flow - only visible in chat view -->
+ <div
+ class="${this._todoPanelVisible && this.viewMode === "chat"
+ ? "block"
+ : "hidden"} fixed top-12 right-4 w-[400px] xl:w-[350px] lg:w-[300px] md:hidden z-[100] transition-[bottom] duration-200 ease-in-out"
+ style="bottom: var(--chat-input-height, 90px); background: linear-gradient(to bottom, #fafafa 0%, #fafafa 90%, rgba(250, 250, 250, 0.5) 95%, rgba(250, 250, 250, 0.2) 100%); border-left: 1px solid #e0e0e0;"
+ >
+ <sketch-todo-panel
+ .visible=${this._todoPanelVisible && this.viewMode === "chat"}
+ ></sketch-todo-panel>
+ </div>
+ <!-- Diff2 View -->
+ <div
+ class="diff2-view ${this.viewMode === "diff2"
+ ? "view-active flex-1 overflow-hidden min-h-0 flex flex-col h-full"
+ : "hidden"} w-full h-full"
+ >
+ <sketch-diff2-view
+ .commit=${this.currentCommitHash}
+ .gitService=${new DefaultGitDataService()}
+ @diff-comment="${this._handleDiffComment}"
+ ></sketch-diff2-view>
+ </div>
+
+ <!-- Terminal View -->
+ <div
+ class="terminal-view ${this.viewMode === "terminal"
+ ? "view-active flex flex-col"
+ : "hidden"} w-full h-full"
+ >
+ <sketch-terminal></sketch-terminal>
</div>
</div>
-
- <!-- Todo panel positioned outside the main flow - only visible in chat view -->
- <div
- class="todo-panel-container ${this._todoPanelVisible &&
- this.viewMode === "chat"
- ? "visible"
- : ""}"
- >
- <sketch-todo-panel
- .visible=${this._todoPanelVisible && this.viewMode === "chat"}
- ></sketch-todo-panel>
- </div>
- <div
- class="diff2-view ${this.viewMode === "diff2" ? "view-active" : ""}"
- >
- <sketch-diff2-view
- .commit=${this.currentCommitHash}
- .gitService=${new DefaultGitDataService()}
- @diff-comment="${this._handleDiffComment}"
- ></sketch-diff2-view>
- </div>
-
- <div
- class="terminal-view ${this.viewMode === "terminal"
- ? "view-active"
- : ""}"
- >
- <sketch-terminal></sketch-terminal>
- </div>
</div>
- </div>
- <div id="chat-input">
- <sketch-chat-input @send-chat="${this._sendChat}"></sketch-chat-input>
+ <!-- Chat input fixed at bottom -->
+ <div
+ id="chat-input"
+ class="self-end w-full shadow-[0_-2px_10px_rgba(0,0,0,0.1)]"
+ >
+ <sketch-chat-input @send-chat="${this._sendChat}"></sketch-chat-input>
+ </div>
</div>
`;
}
diff --git a/webui/src/web-components/sketch-tailwind-element.ts b/webui/src/web-components/sketch-tailwind-element.ts
new file mode 100644
index 0000000..85f380b
--- /dev/null
+++ b/webui/src/web-components/sketch-tailwind-element.ts
@@ -0,0 +1,11 @@
+import { LitElement } from "lit";
+
+export class SketchTailwindElement extends LitElement {
+ // Disable shadow DOM for better integration with tailwind.
+ // Inspired by:
+ // https://lengrand.fr/a-simple-setup-to-use-litelement-with-tailwindcss-for-small-projects/
+
+ createRenderRoot() {
+ return this;
+ }
+}