Spaces:
Sleeping
Sleeping
Deploy Mplus Studio
Browse files- .gitattributes +3 -0
- e2e/studio-flow.spec.ts +3 -3
- public/demo-menu-burger.jpg +3 -0
- public/demo-menu-drink.jpg +3 -0
- public/demo-menu-fries.jpg +3 -0
- src/app/app.component.spec.ts +2 -2
- src/app/app.html +27 -9
- src/app/app.scss +69 -70
- src/app/app.ts +42 -3
.gitattributes
CHANGED
|
@@ -40,3 +40,6 @@ docs/demo-menu-result-20260531022723.png filter=lfs diff=lfs merge=lfs -text
|
|
| 40 |
e2e/fixtures/menu-burger.jpg filter=lfs diff=lfs merge=lfs -text
|
| 41 |
e2e/fixtures/menu-drink.jpg filter=lfs diff=lfs merge=lfs -text
|
| 42 |
e2e/fixtures/menu-fries.jpg filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
e2e/fixtures/menu-burger.jpg filter=lfs diff=lfs merge=lfs -text
|
| 41 |
e2e/fixtures/menu-drink.jpg filter=lfs diff=lfs merge=lfs -text
|
| 42 |
e2e/fixtures/menu-fries.jpg filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
public/demo-menu-burger.jpg filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
public/demo-menu-drink.jpg filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
public/demo-menu-fries.jpg filter=lfs diff=lfs merge=lfs -text
|
e2e/studio-flow.spec.ts
CHANGED
|
@@ -36,7 +36,7 @@ test('generates a real menu deal image through Kaggle', async ({ page }) => {
|
|
| 36 |
await page.goto('/');
|
| 37 |
await waitForDemo();
|
| 38 |
|
| 39 |
-
await expect(page.getByRole('heading', { name: 'Maak
|
| 40 |
await expect(page.getByRole('button', { name: 'Maak nieuwe beelden' })).toBeDisabled();
|
| 41 |
|
| 42 |
await page.locator('input[type="file"]:not([webkitdirectory])').setInputFiles(demoImagePaths);
|
|
@@ -47,9 +47,9 @@ test('generates a real menu deal image through Kaggle', async ({ page }) => {
|
|
| 47 |
await expect(page.getByText('menu-fries.jpg')).toBeVisible();
|
| 48 |
|
| 49 |
await page.getByLabel('Type beeld').selectOption('menu');
|
| 50 |
-
await page.getByLabel('
|
| 51 |
await page.getByLabel('Formaat').selectOption('1:1');
|
| 52 |
-
await page.getByLabel('
|
| 53 |
await waitForDemo();
|
| 54 |
|
| 55 |
const generationResponsePromise = page.waitForResponse('http://127.0.0.1:3000/api/generations', { timeout: 7_200_000 });
|
|
|
|
| 36 |
await page.goto('/');
|
| 37 |
await waitForDemo();
|
| 38 |
|
| 39 |
+
await expect(page.getByRole('heading', { name: 'Maak een menu-afbeelding.' })).toBeVisible();
|
| 40 |
await expect(page.getByRole('button', { name: 'Maak nieuwe beelden' })).toBeDisabled();
|
| 41 |
|
| 42 |
await page.locator('input[type="file"]:not([webkitdirectory])').setInputFiles(demoImagePaths);
|
|
|
|
| 47 |
await expect(page.getByText('menu-fries.jpg')).toBeVisible();
|
| 48 |
|
| 49 |
await page.getByLabel('Type beeld').selectOption('menu');
|
| 50 |
+
await page.getByLabel('Stijl').selectOption('studio');
|
| 51 |
await page.getByLabel('Formaat').selectOption('1:1');
|
| 52 |
+
await page.getByLabel('Instructie').fill(prompt);
|
| 53 |
await waitForDemo();
|
| 54 |
|
| 55 |
const generationResponsePromise = page.waitForResponse('http://127.0.0.1:3000/api/generations', { timeout: 7_200_000 });
|
public/demo-menu-burger.jpg
ADDED
|
Git LFS Details
|
public/demo-menu-drink.jpg
ADDED
|
Git LFS Details
|
public/demo-menu-fries.jpg
ADDED
|
Git LFS Details
|
src/app/app.component.spec.ts
CHANGED
|
@@ -13,8 +13,8 @@ describe('App component', () => {
|
|
| 13 |
it('renders the studio shell and disabled generate button', () => {
|
| 14 |
const text = fixture.nativeElement.textContent.replace(/\s+/g, ' ').trim();
|
| 15 |
|
| 16 |
-
expect(text).toContain('MplusKassa Studio
|
| 17 |
-
expect(text).toContain('Maak
|
| 18 |
expect(fixture.nativeElement.querySelector('.primary').disabled).toBe(true);
|
| 19 |
});
|
| 20 |
});
|
|
|
|
| 13 |
it('renders the studio shell and disabled generate button', () => {
|
| 14 |
const text = fixture.nativeElement.textContent.replace(/\s+/g, ' ').trim();
|
| 15 |
|
| 16 |
+
expect(text).toContain('MplusKassa Studio');
|
| 17 |
+
expect(text).toContain('Maak een menu-afbeelding.');
|
| 18 |
expect(fixture.nativeElement.querySelector('.primary').disabled).toBe(true);
|
| 19 |
});
|
| 20 |
});
|
src/app/app.html
CHANGED
|
@@ -1,15 +1,15 @@
|
|
| 1 |
<main class="studio-shell">
|
| 2 |
<nav class="top-bar" aria-label="Studio status">
|
| 3 |
<strong>MplusKassa Studio</strong>
|
| 4 |
-
<span>
|
| 5 |
</nav>
|
| 6 |
|
| 7 |
<section class="workflow-board" aria-label="Beeldworkflow">
|
| 8 |
<header class="board-header">
|
| 9 |
<div>
|
| 10 |
-
<p class="eyebrow">
|
| 11 |
-
<h1>Maak
|
| 12 |
-
<p class="intro">
|
| 13 |
</div>
|
| 14 |
<p class="next-action">{{ nextAction() }}</p>
|
| 15 |
</header>
|
|
@@ -28,9 +28,27 @@
|
|
| 28 |
|
| 29 |
<section class="action-panel" aria-label="Volgende actie">
|
| 30 |
<div class="task-heading">
|
| 31 |
-
<p class="eyebrow">
|
| 32 |
-
<h2>
|
| 33 |
-
<p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
</div>
|
| 35 |
|
| 36 |
<div class="upload-card">
|
|
@@ -71,7 +89,7 @@
|
|
| 71 |
</label>
|
| 72 |
|
| 73 |
<label>
|
| 74 |
-
|
| 75 |
<select
|
| 76 |
[ngModel]="preset()"
|
| 77 |
(ngModelChange)="preset.set($event)"
|
|
@@ -96,7 +114,7 @@
|
|
| 96 |
</label>
|
| 97 |
|
| 98 |
<label class="prompt-field">
|
| 99 |
-
|
| 100 |
<textarea
|
| 101 |
[ngModel]="prompt()"
|
| 102 |
(ngModelChange)="prompt.set($event)"
|
|
|
|
| 1 |
<main class="studio-shell">
|
| 2 |
<nav class="top-bar" aria-label="Studio status">
|
| 3 |
<strong>MplusKassa Studio</strong>
|
| 4 |
+
<span>Menu deal staat klaar</span>
|
| 5 |
</nav>
|
| 6 |
|
| 7 |
<section class="workflow-board" aria-label="Beeldworkflow">
|
| 8 |
<header class="board-header">
|
| 9 |
<div>
|
| 10 |
+
<p class="eyebrow">Stap 1 / beeldbron</p>
|
| 11 |
+
<h1>Maak een menu-afbeelding.</h1>
|
| 12 |
+
<p class="intro">Start met het voorbeeldmenu of kies eigen foto’s. Daarna maakt Studio één vierkante compositie.</p>
|
| 13 |
</div>
|
| 14 |
<p class="next-action">{{ nextAction() }}</p>
|
| 15 |
</header>
|
|
|
|
| 28 |
|
| 29 |
<section class="action-panel" aria-label="Volgende actie">
|
| 30 |
<div class="task-heading">
|
| 31 |
+
<p class="eyebrow">Snel starten</p>
|
| 32 |
+
<h2>Gebruik het voorbeeldmenu</h2>
|
| 33 |
+
<p>Laadt direct burger, drank en friet. Geen instellingen nodig om de eerste generatie te testen.</p>
|
| 34 |
+
</div>
|
| 35 |
+
|
| 36 |
+
<button
|
| 37 |
+
class="example-menu"
|
| 38 |
+
type="button"
|
| 39 |
+
(click)="onUseExampleMenu()"
|
| 40 |
+
>Gebruik voorbeeldmenu</button>
|
| 41 |
+
|
| 42 |
+
<div class="example-strip" aria-label="Voorbeeldbeelden">
|
| 43 |
+
@for (exampleImage of exampleImages; track exampleImage.name) {
|
| 44 |
+
<figure>
|
| 45 |
+
<img
|
| 46 |
+
[src]="exampleImage.url"
|
| 47 |
+
[alt]="exampleImage.label"
|
| 48 |
+
/>
|
| 49 |
+
<figcaption>{{ exampleImage.label }}</figcaption>
|
| 50 |
+
</figure>
|
| 51 |
+
}
|
| 52 |
</div>
|
| 53 |
|
| 54 |
<div class="upload-card">
|
|
|
|
| 89 |
</label>
|
| 90 |
|
| 91 |
<label>
|
| 92 |
+
Stijl
|
| 93 |
<select
|
| 94 |
[ngModel]="preset()"
|
| 95 |
(ngModelChange)="preset.set($event)"
|
|
|
|
| 114 |
</label>
|
| 115 |
|
| 116 |
<label class="prompt-field">
|
| 117 |
+
Instructie
|
| 118 |
<textarea
|
| 119 |
[ngModel]="prompt()"
|
| 120 |
(ngModelChange)="prompt.set($event)"
|
src/app/app.scss
CHANGED
|
@@ -1,89 +1,88 @@
|
|
| 1 |
-
:host { display: block; min-height: 100dvh; color: #
|
| 2 |
-
.studio-shell { width: min(
|
| 3 |
-
.top-bar
|
| 4 |
-
.top-bar
|
| 5 |
-
.top-bar span { color: #
|
| 6 |
-
.workflow-board
|
| 7 |
-
.
|
| 8 |
-
.board-header
|
| 9 |
-
.eyebrow { margin: 0 0
|
| 10 |
h1, h2, p { margin: 0; }
|
| 11 |
-
h1 { max-width:
|
| 12 |
-
.intro { max-width:
|
| 13 |
-
.next-action {
|
| 14 |
-
.workflow-grid { display: grid; grid-template-columns:
|
| 15 |
.step-rail, .action-panel, .proof-panel { min-height: 0; }
|
| 16 |
-
.
|
| 17 |
-
.monitor-steps
|
| 18 |
-
.monitor-steps li
|
| 19 |
-
.monitor-steps li.active {
|
| 20 |
-
.monitor-steps li.
|
| 21 |
-
.monitor-steps li.
|
| 22 |
-
.monitor-steps
|
| 23 |
-
.monitor-steps
|
| 24 |
-
.
|
| 25 |
-
.
|
| 26 |
-
.task-heading {
|
| 27 |
-
.task-heading
|
| 28 |
-
.
|
| 29 |
-
.
|
| 30 |
-
.
|
| 31 |
-
.
|
| 32 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
.upload-card input { position: absolute; inline-size: 1px; block-size: 1px; opacity: 0; }
|
| 34 |
-
.upload-card span { font-
|
| 35 |
-
.upload-card small, .note, .image-card header p { color: #
|
| 36 |
-
.controls { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap:
|
| 37 |
.prompt-field, .primary { grid-column: 1 / -1; }
|
| 38 |
-
label { display: grid; gap:
|
| 39 |
select, textarea, button { font: inherit; }
|
| 40 |
-
select, textarea { width: 100%; border: 1px solid
|
| 41 |
-
select:focus, textarea:focus { border-color: #
|
| 42 |
-
textarea { min-height:
|
| 43 |
-
button { border:
|
| 44 |
button:disabled { cursor: not-allowed; opacity: 0.5; }
|
| 45 |
-
.primary { min-height:
|
| 46 |
-
.primary:hover:not(:disabled) { background: #
|
| 47 |
-
.proof-panel { padding:
|
| 48 |
-
.proof-panel h2 { font-size: 1.
|
| 49 |
-
.proof-panel dl { display: grid;
|
| 50 |
-
.proof-panel div { padding
|
| 51 |
-
dt { font-size: 0.
|
| 52 |
dd { margin: 3px 0 0; overflow: hidden; font-weight: 900; text-overflow: ellipsis; white-space: nowrap; }
|
| 53 |
-
details { margin-top:
|
| 54 |
-
summary { cursor: pointer; font-size: 0.82rem; font-weight: 900;
|
| 55 |
-
details ol { display: grid; gap:
|
| 56 |
-
.empty-state, .status-message { margin-top: 12px; padding:
|
| 57 |
-
.
|
| 58 |
-
.
|
| 59 |
-
.image-
|
| 60 |
-
.image-card {
|
| 61 |
-
.image-card header { display: flex; justify-content: space-between; gap: 14px; align-items: flex-start; margin-bottom: 14px; }
|
| 62 |
.image-card h2 { overflow: hidden; max-width: 520px; font-size: 1rem; text-overflow: ellipsis; white-space: nowrap; }
|
| 63 |
-
.ghost, .variant-row button { background: #
|
| 64 |
.preview-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
| 65 |
-
figure { margin: 0; overflow: hidden; border: 1px solid
|
| 66 |
img { display: block; width: 100%; aspect-ratio: 16 / 10; object-fit: cover; }
|
| 67 |
-
figcaption { padding:
|
| 68 |
-
.generated { background: #
|
| 69 |
.variant-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
|
| 70 |
-
.variant-row button:hover, .ghost:hover { background: #
|
| 71 |
-
.variant-row button.active { background: #
|
| 72 |
-
.note { margin-top: 12px; font-size: 0.
|
| 73 |
|
| 74 |
@media (max-width: 980px) {
|
| 75 |
.workflow-board { min-height: auto; }
|
| 76 |
.board-header, .workflow-grid, .preview-pair { grid-template-columns: 1fr; }
|
| 77 |
-
.
|
| 78 |
-
.step-rail { order: 2; }
|
| 79 |
-
.proof-panel { order: 3; }
|
| 80 |
}
|
| 81 |
|
| 82 |
@media (max-width: 640px) {
|
| 83 |
-
.studio-shell { width: min(100% -
|
| 84 |
-
.top-bar
|
| 85 |
-
.
|
| 86 |
-
|
| 87 |
-
.upload-card, .controls { grid-template-columns: 1fr; }
|
| 88 |
-
h1 { font-size: clamp(2.2rem, 14vw, 3.6rem); }
|
| 89 |
}
|
|
|
|
| 1 |
+
:host { display: block; min-height: 100dvh; color: #111; background: #f4f4f1; font-family: Arial, Helvetica, sans-serif; }
|
| 2 |
+
.studio-shell { width: min(1180px, calc(100% - 24px)); margin: 0 auto; padding: 12px 0 32px; }
|
| 3 |
+
.top-bar, .workflow-board, .empty-state, .image-card { border: 1px solid #111; background: #fff; }
|
| 4 |
+
.top-bar { display: grid; grid-template-columns: 1fr auto; gap: 16px; margin-bottom: 12px; padding: 10px 12px; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.08em; }
|
| 5 |
+
.top-bar span { color: #666; }
|
| 6 |
+
.workflow-board { min-height: calc(100dvh - 72px); display: grid; grid-template-rows: auto 1fr; }
|
| 7 |
+
.board-header { display: grid; grid-template-columns: minmax(0, 1fr) 320px; border-bottom: 1px solid #111; }
|
| 8 |
+
.board-header > div { padding: 18px; }
|
| 9 |
+
.eyebrow { margin: 0 0 10px; color: #e42313; font-size: 0.72rem; font-weight: 900; letter-spacing: 0.14em; text-transform: uppercase; }
|
| 10 |
h1, h2, p { margin: 0; }
|
| 11 |
+
h1 { max-width: 680px; font-size: clamp(2.4rem, 6vw, 5.6rem); line-height: 0.86; letter-spacing: -0.07em; }
|
| 12 |
+
.intro { max-width: 54ch; margin-top: 14px; color: #444; font-size: 1rem; line-height: 1.45; }
|
| 13 |
+
.next-action { display: grid; align-content: end; padding: 18px; border-left: 1px solid #111; background: #e42313; color: #fff; font-size: 1.04rem; font-weight: 900; line-height: 1.22; }
|
| 14 |
+
.workflow-grid { display: grid; grid-template-columns: 190px minmax(0, 1fr) 250px; min-height: 0; }
|
| 15 |
.step-rail, .action-panel, .proof-panel { min-height: 0; }
|
| 16 |
+
.step-rail { border-right: 1px solid #111; background: #f8f8f5; }
|
| 17 |
+
.monitor-steps { display: grid; margin: 0; padding: 0; list-style: none; }
|
| 18 |
+
.monitor-steps li { display: grid; gap: 5px; padding: 12px; border-bottom: 1px solid #111; color: #666; }
|
| 19 |
+
.monitor-steps li.active { background: #111; color: #fff; }
|
| 20 |
+
.monitor-steps li.completed { background: #e8f1e4; color: #16551b; }
|
| 21 |
+
.monitor-steps li.failed { background: #e42313; color: #fff; }
|
| 22 |
+
.monitor-steps strong { font-size: 0.9rem; }
|
| 23 |
+
.monitor-steps span { font-size: 0.76rem; line-height: 1.28; }
|
| 24 |
+
.action-panel { display: grid; gap: 12px; align-content: start; padding: 12px; }
|
| 25 |
+
.task-heading { display: grid; gap: 8px; padding: 14px; border: 1px solid #111; background: #f8f8f5; }
|
| 26 |
+
.task-heading h2 { font-size: clamp(1.4rem, 2.5vw, 2.4rem); line-height: 0.95; letter-spacing: -0.05em; }
|
| 27 |
+
.task-heading p:last-child { max-width: 56ch; color: #444; line-height: 1.4; }
|
| 28 |
+
.example-menu { min-height: 52px; background: #111; color: #fff; }
|
| 29 |
+
.example-menu:hover { background: #e42313; }
|
| 30 |
+
.example-strip { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0; border: 1px solid #111; border-right: 0; }
|
| 31 |
+
.example-strip figure { border: 0; border-right: 1px solid #111; }
|
| 32 |
+
.example-strip img { aspect-ratio: 1 / 1; }
|
| 33 |
+
.upload-card { display: grid; grid-template-columns: 1fr 1fr; gap: 0; border: 1px solid #111; border-bottom: 0; }
|
| 34 |
+
.upload-card label { min-height: 92px; display: grid; place-content: center; gap: 5px; cursor: pointer; border-bottom: 1px solid #111; background: #fff; text-align: center; transition: background 120ms ease, color 120ms ease; }
|
| 35 |
+
.upload-card label + label { border-left: 1px solid #111; }
|
| 36 |
+
.upload-card label:hover { background: #f3f3ef; }
|
| 37 |
.upload-card input { position: absolute; inline-size: 1px; block-size: 1px; opacity: 0; }
|
| 38 |
+
.upload-card span { font-weight: 900; }
|
| 39 |
+
.upload-card small, .note, .image-card header p { color: #666; }
|
| 40 |
+
.controls { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0; border-top: 1px solid #111; border-left: 1px solid #111; }
|
| 41 |
.prompt-field, .primary { grid-column: 1 / -1; }
|
| 42 |
+
label { display: grid; gap: 6px; padding: 10px; border-right: 1px solid #111; border-bottom: 1px solid #111; font-size: 0.68rem; font-weight: 900; text-transform: uppercase; letter-spacing: 0.09em; color: #555; }
|
| 43 |
select, textarea, button { font: inherit; }
|
| 44 |
+
select, textarea { width: 100%; border: 1px solid #bbb; border-radius: 0; padding: 9px; background: #fff; color: #111; text-transform: none; letter-spacing: normal; outline: none; }
|
| 45 |
+
select:focus, textarea:focus { border-color: #e42313; box-shadow: 0 0 0 2px rgba(228, 35, 19, 0.14); }
|
| 46 |
+
textarea { min-height: 66px; resize: vertical; line-height: 1.35; }
|
| 47 |
+
button { border: 1px solid #111; border-radius: 0; padding: 11px 14px; font-weight: 900; cursor: pointer; transition: background 120ms ease, color 120ms ease, opacity 120ms ease; }
|
| 48 |
button:disabled { cursor: not-allowed; opacity: 0.5; }
|
| 49 |
+
.primary { min-height: 46px; background: #e42313; color: #fff; }
|
| 50 |
+
.primary:hover:not(:disabled) { background: #111; }
|
| 51 |
+
.proof-panel { padding: 12px; border-left: 1px solid #111; background: #f8f8f5; }
|
| 52 |
+
.proof-panel h2 { font-size: 1.08rem; line-height: 1.08; letter-spacing: -0.025em; }
|
| 53 |
+
.proof-panel dl { display: grid; margin: 16px 0 0; border-top: 1px solid #111; }
|
| 54 |
+
.proof-panel div { padding: 10px 0; border-bottom: 1px solid #111; }
|
| 55 |
+
dt { font-size: 0.64rem; font-weight: 900; letter-spacing: 0.1em; text-transform: uppercase; color: #666; }
|
| 56 |
dd { margin: 3px 0 0; overflow: hidden; font-weight: 900; text-overflow: ellipsis; white-space: nowrap; }
|
| 57 |
+
details { margin-top: 14px; padding-top: 12px; border-top: 1px solid #111; }
|
| 58 |
+
summary { cursor: pointer; font-size: 0.82rem; font-weight: 900; }
|
| 59 |
+
details ol { display: grid; gap: 6px; margin: 10px 0 0; padding-left: 18px; color: #555; font-size: 0.8rem; line-height: 1.3; }
|
| 60 |
+
.empty-state, .status-message { margin-top: 12px; padding: 16px; }
|
| 61 |
+
.status-message { background: #e42313; color: #fff; font-weight: 900; }
|
| 62 |
+
.image-grid { display: grid; gap: 12px; margin-top: 12px; }
|
| 63 |
+
.image-card { padding: 12px; }
|
| 64 |
+
.image-card header { display: flex; justify-content: space-between; gap: 14px; align-items: flex-start; margin-bottom: 12px; }
|
|
|
|
| 65 |
.image-card h2 { overflow: hidden; max-width: 520px; font-size: 1rem; text-overflow: ellipsis; white-space: nowrap; }
|
| 66 |
+
.ghost, .variant-row button { background: #fff; color: #111; }
|
| 67 |
.preview-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
| 68 |
+
figure { margin: 0; overflow: hidden; border: 1px solid #111; background: #ddd; }
|
| 69 |
img { display: block; width: 100%; aspect-ratio: 16 / 10; object-fit: cover; }
|
| 70 |
+
figcaption { padding: 8px 10px; border-top: 1px solid #111; background: #fff; font-size: 0.76rem; font-weight: 900; color: #333; }
|
| 71 |
+
.generated { background: #111; }
|
| 72 |
.variant-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
|
| 73 |
+
.variant-row button:hover, .ghost:hover { background: #f3f3ef; }
|
| 74 |
+
.variant-row button.active { background: #111; color: #fff; }
|
| 75 |
+
.note { margin-top: 12px; font-size: 0.9rem; line-height: 1.4; }
|
| 76 |
|
| 77 |
@media (max-width: 980px) {
|
| 78 |
.workflow-board { min-height: auto; }
|
| 79 |
.board-header, .workflow-grid, .preview-pair { grid-template-columns: 1fr; }
|
| 80 |
+
.next-action, .proof-panel, .step-rail { border-left: 0; border-right: 0; }
|
|
|
|
|
|
|
| 81 |
}
|
| 82 |
|
| 83 |
@media (max-width: 640px) {
|
| 84 |
+
.studio-shell { width: min(100% - 18px, 720px); padding-top: 9px; }
|
| 85 |
+
.top-bar, .upload-card, .controls, .example-strip { grid-template-columns: 1fr; }
|
| 86 |
+
.upload-card label + label, .example-strip figure { border-left: 0; }
|
| 87 |
+
h1 { font-size: clamp(2.3rem, 16vw, 3.8rem); }
|
|
|
|
|
|
|
| 88 |
}
|
src/app/app.ts
CHANGED
|
@@ -3,6 +3,12 @@ import { Component, computed, signal } from '@angular/core';
|
|
| 3 |
import { FormsModule } from '@angular/forms';
|
| 4 |
import { createImages, releaseImage, StudioImage } from './image-file';
|
| 5 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
type Variant = {
|
| 7 |
id: string;
|
| 8 |
label: string;
|
|
@@ -77,11 +83,11 @@ function getGenerationEndpoint(): string {
|
|
| 77 |
export class App {
|
| 78 |
protected readonly images = signal<StudioImage[]>([]);
|
| 79 |
protected readonly prompt = signal(
|
| 80 |
-
'Maak
|
| 81 |
);
|
| 82 |
protected readonly preset = signal('studio');
|
| 83 |
-
protected readonly ratio = signal('
|
| 84 |
-
protected readonly mode = signal<'product' | 'menu'>('
|
| 85 |
protected readonly isGenerating = signal(false);
|
| 86 |
protected readonly generationError = signal<string | undefined>(undefined);
|
| 87 |
protected readonly monitor = signal<MonitorState>({
|
|
@@ -112,6 +118,11 @@ export class App {
|
|
| 112 |
note: 'Meer diepte, luxe uitstraling en sterke schaduw.',
|
| 113 |
},
|
| 114 |
];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
|
| 116 |
protected readonly hasImages = computed(() => this.images().length > 0);
|
| 117 |
public readonly selectedImageCountLabel = computed(() => {
|
|
@@ -185,6 +196,28 @@ export class App {
|
|
| 185 |
input.value = '';
|
| 186 |
}
|
| 187 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
protected async onGenerateBatch(): Promise<void> {
|
| 189 |
if (!this.hasImages()) {
|
| 190 |
return;
|
|
@@ -262,6 +295,12 @@ export class App {
|
|
| 262 |
this.images.update((images) => images.filter((item) => item.id !== imageId));
|
| 263 |
}
|
| 264 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
private findVariant(variantId: string): Variant {
|
| 266 |
return this.variants.find((variant) => variant.id === variantId) ?? this.variants[0];
|
| 267 |
}
|
|
|
|
| 3 |
import { FormsModule } from '@angular/forms';
|
| 4 |
import { createImages, releaseImage, StudioImage } from './image-file';
|
| 5 |
|
| 6 |
+
type ExampleImage = {
|
| 7 |
+
name: string;
|
| 8 |
+
label: string;
|
| 9 |
+
url: string;
|
| 10 |
+
};
|
| 11 |
+
|
| 12 |
type Variant = {
|
| 13 |
id: string;
|
| 14 |
label: string;
|
|
|
|
| 83 |
export class App {
|
| 84 |
protected readonly images = signal<StudioImage[]>([]);
|
| 85 |
protected readonly prompt = signal(
|
| 86 |
+
'Maak een professionele fastfood menu deal met burger, drinken en friet op een schone witte menu-achtergrond',
|
| 87 |
);
|
| 88 |
protected readonly preset = signal('studio');
|
| 89 |
+
protected readonly ratio = signal('1:1');
|
| 90 |
+
protected readonly mode = signal<'product' | 'menu'>('menu');
|
| 91 |
protected readonly isGenerating = signal(false);
|
| 92 |
protected readonly generationError = signal<string | undefined>(undefined);
|
| 93 |
protected readonly monitor = signal<MonitorState>({
|
|
|
|
| 118 |
note: 'Meer diepte, luxe uitstraling en sterke schaduw.',
|
| 119 |
},
|
| 120 |
];
|
| 121 |
+
protected readonly exampleImages: ExampleImage[] = [
|
| 122 |
+
{ name: 'demo-menu-burger.jpg', label: 'Burger', url: '/demo-menu-burger.jpg' },
|
| 123 |
+
{ name: 'demo-menu-drink.jpg', label: 'Drank', url: '/demo-menu-drink.jpg' },
|
| 124 |
+
{ name: 'demo-menu-fries.jpg', label: 'Friet', url: '/demo-menu-fries.jpg' },
|
| 125 |
+
];
|
| 126 |
|
| 127 |
protected readonly hasImages = computed(() => this.images().length > 0);
|
| 128 |
public readonly selectedImageCountLabel = computed(() => {
|
|
|
|
| 196 |
input.value = '';
|
| 197 |
}
|
| 198 |
|
| 199 |
+
protected async onUseExampleMenu(): Promise<void> {
|
| 200 |
+
const files: File[] = [];
|
| 201 |
+
for (const exampleImage of this.exampleImages) {
|
| 202 |
+
const response = await fetch(exampleImage.url);
|
| 203 |
+
const imageBlob = await response.blob();
|
| 204 |
+
files.push(new File([imageBlob], exampleImage.name, { type: imageBlob.type }));
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
this.releaseCurrentImages();
|
| 208 |
+
this.mode.set('menu');
|
| 209 |
+
this.preset.set('studio');
|
| 210 |
+
this.ratio.set('1:1');
|
| 211 |
+
this.prompt.set('Maak een professionele fastfood menu deal met burger, drinken en friet op een schone witte menu-achtergrond');
|
| 212 |
+
this.images.set(createImages(files));
|
| 213 |
+
this.monitor.update((current) => ({
|
| 214 |
+
...current,
|
| 215 |
+
status: 'idle',
|
| 216 |
+
message: 'Voorbeeldmenu is geladen. Maak nu een nieuw beeld.',
|
| 217 |
+
events: [...current.events, 'Voorbeeldbeelden geladen.'],
|
| 218 |
+
}));
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
protected async onGenerateBatch(): Promise<void> {
|
| 222 |
if (!this.hasImages()) {
|
| 223 |
return;
|
|
|
|
| 295 |
this.images.update((images) => images.filter((item) => item.id !== imageId));
|
| 296 |
}
|
| 297 |
|
| 298 |
+
private releaseCurrentImages(): void {
|
| 299 |
+
for (const image of this.images()) {
|
| 300 |
+
releaseImage(image);
|
| 301 |
+
}
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
private findVariant(variantId: string): Variant {
|
| 305 |
return this.variants.find((variant) => variant.id === variantId) ?? this.variants[0];
|
| 306 |
}
|