{#if suggestionLoading}
Generating a task suggestion
@@ -851,6 +863,9 @@
opacity: 0.65;
}
}
+ :global(.generated-workspace) {
+ min-width: 0;
+ }
:global(.generated-workspace .surface-card) {
display: grid;
gap: 1rem;
@@ -940,7 +955,9 @@
cursor: pointer;
}
:global(.generated-workspace .surface-table) {
+ display: block;
width: 100%;
+ overflow-x: auto;
border-collapse: collapse;
font-size: 0.88rem;
}
@@ -966,4 +983,17 @@
font-size: 0.85rem;
font-weight: 600;
}
+ @media (max-width: 639px) {
+ :global(.generated-workspace .surface-item) {
+ align-items: flex-start;
+ flex-direction: column;
+ gap: 0.5rem;
+ }
+ :global(.generated-workspace button),
+ :global(.generated-workspace a.surface-action) {
+ width: 100%;
+ justify-content: center;
+ text-align: center;
+ }
+ }
diff --git a/examples/sveltekit/src/routes/customers/+page.svelte b/examples/sveltekit/src/routes/customers/+page.svelte
index 281a6de..15630c1 100644
--- a/examples/sveltekit/src/routes/customers/+page.svelte
+++ b/examples/sveltekit/src/routes/customers/+page.svelte
@@ -48,13 +48,13 @@
-
+
-
+
Customers
@@ -98,30 +98,50 @@
{#each visibleCustomers as customer}
{customer.name}
-
+
{customer.email} · since {new Date(
customer.createdAt,
).toLocaleDateString()}
- {customer.loyaltyTier}
- {customerOrders(customer.customerId).length}
- {reservationCount(customer.customerId)}
- ${paidSpend(customer.customerId).toFixed(2)}
+
+ Loyalty
+ {customer.loyaltyTier}
+
+
+ Orders
+ {customerOrders(customer.customerId).length}
+
+
+ Reservations
+ {reservationCount(customer.customerId)}
+
+
+ Paid spend
+ ${paidSpend(customer.customerId).toFixed(2)}
+
{:else}
diff --git a/examples/sveltekit/src/routes/menus/+page.svelte b/examples/sveltekit/src/routes/menus/+page.svelte
index 39b246a..292ad67 100644
--- a/examples/sveltekit/src/routes/menus/+page.svelte
+++ b/examples/sveltekit/src/routes/menus/+page.svelte
@@ -62,9 +62,9 @@
-
+
-
+
Menus and pricing
@@ -125,7 +125,7 @@
{/each}
-
+
{selectedMenu.name}
@@ -140,7 +140,7 @@
{#each visibleItems as item}
-
+
${item.price.toFixed(2)}
diff --git a/examples/sveltekit/src/routes/orders/+page.svelte b/examples/sveltekit/src/routes/orders/+page.svelte
index 26c178a..c24cafe 100644
--- a/examples/sveltekit/src/routes/orders/+page.svelte
+++ b/examples/sveltekit/src/routes/orders/+page.svelte
@@ -85,13 +85,13 @@
-
+
-
+
Orders
@@ -147,7 +147,7 @@
{#each visibleOrders as order}
{order.status}
-
+
{order.orderId} · {order.serviceType} · {new Date(
order.createdAt,
).toLocaleString()}
@@ -206,9 +206,10 @@
class="space-y-1 border-t border-neutral-100 pt-3 text-xs text-neutral-600"
>
{#each order.lines as line}
-
- {line.quantity}× {line.itemName}
+ {line.quantity}× {line.itemName}${(line.unitPrice * line.quantity).toFixed(2)}
diff --git a/examples/sveltekit/src/routes/users/+page.svelte b/examples/sveltekit/src/routes/users/+page.svelte
index d7f0a1b..99eaa77 100644
--- a/examples/sveltekit/src/routes/users/+page.svelte
+++ b/examples/sveltekit/src/routes/users/+page.svelte
@@ -35,9 +35,9 @@
-
+
-
+
Restaurant staff
@@ -65,21 +65,41 @@
{#each users as user}
{user.name}
- {user.role}
- {assignedOrders(user.userId).length}
- ${paidSales(user.userId).toFixed(2)}
- {scheduledHours(user.userId).toFixed(1)}h
+
+ Role
+ {user.role}
+
+
+ Orders
+ {assignedOrders(user.userId).length}
+
+
+ Paid sales
+ ${paidSales(user.userId).toFixed(2)}
+
+
+ Shift hours
+ {scheduledHours(user.userId).toFixed(1)}h
+
{/each}
diff --git a/src/ui-runtime.ts b/src/ui-runtime.ts
index 7883358..9a8dd08 100644
--- a/src/ui-runtime.ts
+++ b/src/ui-runtime.ts
@@ -413,28 +413,24 @@ function interpolationContext(
before: string,
after: string,
): TemplateInterpolationContext {
- const lastOpen = before.lastIndexOf("<");
- const lastClose = before.lastIndexOf(">");
- const inTag = lastOpen > lastClose;
+ if (!endsInsideTag(before)) return { kind: "text" };
- if (!inTag) return { kind: "text" };
-
- const quotedAttr = before.match(/([:@A-Za-z0-9_-]+)\s*=\s*(["'])$/);
+ const quotedAttr = before.match(/([:@A-Za-z0-9_-]+)\s*=\s*(["'])([^"']*)$/);
if (quotedAttr) {
const attr = quotedAttr[1];
const quote = quotedAttr[2];
-
- if (after[0] !== quote) return { kind: "structure" };
-
+ const prefix = quotedAttr[3];
const next = after[1];
- if (next && next !== ">" && next !== "/" && !/\s/.test(next)) {
- return { kind: "structure" };
- }
-
+ const fillsWholeAttribute =
+ prefix.length === 0 &&
+ after[0] === quote &&
+ (!next || next === ">" || next === "/" || /\s/.test(next));
const lower = attr.toLowerCase();
+
if (booleanAttributes.has(lower)) return { kind: "structure" };
if (eventAttributes.has(lower)) {
+ if (!fillsWholeAttribute) return { kind: "structure" };
return {
kind: "event",
attribute: attr,
@@ -467,6 +463,29 @@ function interpolationContext(
return { kind: "attribute", attribute: attr, quoted: false };
}
+function endsInsideTag(value: string): boolean {
+ let inTag = false;
+ let quote = "";
+
+ for (const character of value) {
+ if (!inTag) {
+ if (character === "<") inTag = true;
+ continue;
+ }
+ if (quote) {
+ if (character === quote) quote = "";
+ continue;
+ }
+ if (character === '"' || character === "'") {
+ quote = character;
+ } else if (character === ">") {
+ inTag = false;
+ }
+ }
+
+ return inTag;
+}
+
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
diff --git a/tests/ui-runtime.test.ts b/tests/ui-runtime.test.ts
index fdfa914..9f754ad 100644
--- a/tests/ui-runtime.test.ts
+++ b/tests/ui-runtime.test.ts
@@ -63,6 +63,25 @@ test("createUiComponent supports delegated SVG events and closure rerendering",
);
});
+test("compileUiTemplate supports mixed quoted attribute interpolation", () => {
+ const html = createUiTemplateTag();
+ const width = 720;
+ const height = 280;
+ const period = "this week";
+
+ const compiled = compileUiTemplate(
+ html`
`,
+ ).unwrap();
+
+ expect(compiled.html).toBe(
+ '
',
+ );
+});
+
test("compileUiTemplate supports boolean attribute interpolation", () => {
const html = createUiTemplateTag();
@@ -288,6 +307,20 @@ test("compileUiTemplate rejects interpolation that changes tag structure", () =>
});
});
+test("compileUiTemplate rejects mixed event-handler attributes", () => {
+ const html = createUiTemplateTag();
+
+ const compiled = compileUiTemplate(
+ html`
`,
+ );
+
+ expect(Result.isError(compiled)).toBe(true);
+ expect(compiled.unwrapError()).toEqual({
+ code: "unsafe_interpolation",
+ message: "Interpolations cannot change tag or attribute structure.",
+ });
+});
+
test("compileUiTemplate rejects unsafe tags", () => {
const html = createUiTemplateTag();