@ssejal8/recipe-ui-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +10 -0
- package/.npmr +1 -0
- package/README.md +354 -0
- package/apps/recipe-web/package.json +31 -0
- package/apps/recipe-web/src/app.css +418 -0
- package/apps/recipe-web/src/app.d.ts +32 -0
- package/apps/recipe-web/src/app.html +25 -0
- package/apps/recipe-web/src/lib/api/mealdb.ts +240 -0
- package/apps/recipe-web/src/lib/components/AppHeader.svelte +290 -0
- package/apps/recipe-web/src/lib/components/ConfirmDialog.svelte +46 -0
- package/apps/recipe-web/src/lib/components/Modal.svelte +33 -0
- package/apps/recipe-web/src/lib/components/RecipeCard.svelte +100 -0
- package/apps/recipe-web/src/lib/components/RecipeForm.svelte +560 -0
- package/apps/recipe-web/src/lib/components/RecipeGrid.svelte +77 -0
- package/apps/recipe-web/src/lib/components/RecipePickerDialog.svelte +185 -0
- package/apps/recipe-web/src/lib/components/Toaster.svelte +111 -0
- package/apps/recipe-web/src/lib/domain/recipe.ts +130 -0
- package/apps/recipe-web/src/lib/domain/validation.ts +189 -0
- package/apps/recipe-web/src/lib/stencil/WebComponent.svelte +145 -0
- package/apps/recipe-web/src/lib/stencil/register.ts +28 -0
- package/apps/recipe-web/src/lib/stores/favorites.svelte.ts +81 -0
- package/apps/recipe-web/src/lib/stores/myRecipes.svelte.ts +98 -0
- package/apps/recipe-web/src/lib/stores/persisted.svelte.ts +73 -0
- package/apps/recipe-web/src/lib/stores/planner.svelte.ts +275 -0
- package/apps/recipe-web/src/lib/stores/theme.svelte.ts +67 -0
- package/apps/recipe-web/src/lib/stores/toast.svelte.ts +66 -0
- package/apps/recipe-web/src/routes/+error.svelte +36 -0
- package/apps/recipe-web/src/routes/+layout.svelte +62 -0
- package/apps/recipe-web/src/routes/+page.svelte +352 -0
- package/apps/recipe-web/src/routes/+page.ts +107 -0
- package/apps/recipe-web/src/routes/favorites/+page.svelte +170 -0
- package/apps/recipe-web/src/routes/my-recipes/+layout.ts +55 -0
- package/apps/recipe-web/src/routes/my-recipes/+page.svelte +180 -0
- package/apps/recipe-web/src/routes/my-recipes/[id]/edit/+page.svelte +104 -0
- package/apps/recipe-web/src/routes/my-recipes/new/+page.svelte +67 -0
- package/apps/recipe-web/src/routes/planner/+page.svelte +422 -0
- package/apps/recipe-web/src/routes/recipes/[id]/+page.svelte +538 -0
- package/apps/recipe-web/src/routes/recipes/[id]/+page.ts +24 -0
- package/apps/recipe-web/static/favicon.svg +8 -0
- package/apps/recipe-web/static/robots.txt +2 -0
- package/apps/recipe-web/svelte.config.js +35 -0
- package/apps/recipe-web/tsconfig.json +14 -0
- package/apps/recipe-web/vite.config.ts +23 -0
- package/package.json +29 -0
- package/packages/recipe-kit-stencil/LICENSE +21 -0
- package/packages/recipe-kit-stencil/README.md +185 -0
- package/packages/recipe-kit-stencil/package-lock.json +4772 -0
- package/packages/recipe-kit-stencil/package.json +91 -0
- package/packages/recipe-kit-stencil/src/components/rk-badge/readme.md +38 -0
- package/packages/recipe-kit-stencil/src/components/rk-badge/rk-badge.css +102 -0
- package/packages/recipe-kit-stencil/src/components/rk-badge/rk-badge.tsx +31 -0
- package/packages/recipe-kit-stencil/src/components/rk-chip/readme.md +52 -0
- package/packages/recipe-kit-stencil/src/components/rk-chip/rk-chip.css +155 -0
- package/packages/recipe-kit-stencil/src/components/rk-chip/rk-chip.tsx +105 -0
- package/packages/recipe-kit-stencil/src/components/rk-empty-state/readme.md +44 -0
- package/packages/recipe-kit-stencil/src/components/rk-empty-state/rk-empty-state.css +64 -0
- package/packages/recipe-kit-stencil/src/components/rk-empty-state/rk-empty-state.tsx +50 -0
- package/packages/recipe-kit-stencil/src/components/rk-meal-slot/readme.md +58 -0
- package/packages/recipe-kit-stencil/src/components/rk-meal-slot/rk-meal-slot.css +225 -0
- package/packages/recipe-kit-stencil/src/components/rk-meal-slot/rk-meal-slot.tsx +182 -0
- package/packages/recipe-kit-stencil/src/components/rk-modal/readme.md +57 -0
- package/packages/recipe-kit-stencil/src/components/rk-modal/rk-modal.css +163 -0
- package/packages/recipe-kit-stencil/src/components/rk-modal/rk-modal.tsx +176 -0
- package/packages/recipe-kit-stencil/src/components/rk-rating/readme.md +42 -0
- package/packages/recipe-kit-stencil/src/components/rk-rating/rk-rating.css +71 -0
- package/packages/recipe-kit-stencil/src/components/rk-rating/rk-rating.tsx +114 -0
- package/packages/recipe-kit-stencil/src/components/rk-recipe-card/readme.md +66 -0
- package/packages/recipe-kit-stencil/src/components/rk-recipe-card/rk-recipe-card.css +280 -0
- package/packages/recipe-kit-stencil/src/components/rk-recipe-card/rk-recipe-card.tsx +179 -0
- package/packages/recipe-kit-stencil/src/components/rk-recipe-card/test/rk-recipe-card.spec.tsx +70 -0
- package/packages/recipe-kit-stencil/src/components/rk-search-bar/readme.md +67 -0
- package/packages/recipe-kit-stencil/src/components/rk-search-bar/rk-search-bar.css +141 -0
- package/packages/recipe-kit-stencil/src/components/rk-search-bar/rk-search-bar.tsx +152 -0
- package/packages/recipe-kit-stencil/src/components/rk-search-bar/test/rk-search-bar.spec.tsx +93 -0
- package/packages/recipe-kit-stencil/src/components/rk-segmented-control/readme.md +41 -0
- package/packages/recipe-kit-stencil/src/components/rk-segmented-control/rk-segmented-control.css +91 -0
- package/packages/recipe-kit-stencil/src/components/rk-segmented-control/rk-segmented-control.tsx +115 -0
- package/packages/recipe-kit-stencil/src/components/rk-tag-input/readme.md +47 -0
- package/packages/recipe-kit-stencil/src/components/rk-tag-input/rk-tag-input.css +146 -0
- package/packages/recipe-kit-stencil/src/components/rk-tag-input/rk-tag-input.tsx +181 -0
- package/packages/recipe-kit-stencil/src/components/rk-tag-input/test/rk-tag-input.spec.tsx +76 -0
- package/packages/recipe-kit-stencil/src/components.d.ts +1192 -0
- package/packages/recipe-kit-stencil/src/global/rk-global.css +104 -0
- package/packages/recipe-kit-stencil/src/index.html +259 -0
- package/packages/recipe-kit-stencil/src/index.ts +6 -0
- package/packages/recipe-kit-stencil/stencil.config.ts +41 -0
- package/packages/recipe-kit-stencil/tsconfig.json +24 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { goto } from '$app/navigation';
|
|
3
|
+
import ConfirmDialog from '$components/ConfirmDialog.svelte';
|
|
4
|
+
import RecipePickerDialog from '$components/RecipePickerDialog.svelte';
|
|
5
|
+
import type { RecipeSummary } from '$domain/recipe';
|
|
6
|
+
import WebComponent from '$lib/stencil/WebComponent.svelte';
|
|
7
|
+
import {
|
|
8
|
+
currentWeekKey,
|
|
9
|
+
dateForDay,
|
|
10
|
+
DAYS,
|
|
11
|
+
formatWeekRange,
|
|
12
|
+
MEALS,
|
|
13
|
+
planner,
|
|
14
|
+
shiftWeekKey,
|
|
15
|
+
type DayKey,
|
|
16
|
+
type MealKey
|
|
17
|
+
} from '$stores/planner.svelte';
|
|
18
|
+
import { toasts } from '$stores/toast.svelte';
|
|
19
|
+
|
|
20
|
+
const weekKey = $derived(planner.selectedWeek);
|
|
21
|
+
const isCurrentWeek = $derived(weekKey === currentWeekKey());
|
|
22
|
+
const plannedCount = $derived(planner.countFor(weekKey));
|
|
23
|
+
const weekRecipes = $derived(planner.recipesFor(weekKey));
|
|
24
|
+
const totalSlots = DAYS.length * MEALS.length;
|
|
25
|
+
|
|
26
|
+
const previousWeekKey = $derived(shiftWeekKey(weekKey, -1));
|
|
27
|
+
const previousWeekCount = $derived(planner.countFor(previousWeekKey));
|
|
28
|
+
|
|
29
|
+
const today = $derived(new Date().toDateString());
|
|
30
|
+
|
|
31
|
+
/* ---------------------------------------------------------- picker dialog */
|
|
32
|
+
|
|
33
|
+
let pickerOpen = $state(false);
|
|
34
|
+
let target = $state<{ day: DayKey; mealType: MealKey } | null>(null);
|
|
35
|
+
|
|
36
|
+
const targetLabel = $derived.by(() => {
|
|
37
|
+
if (!target) return '';
|
|
38
|
+
const day = DAYS.find((entry) => entry.key === target!.day)?.label ?? target.day;
|
|
39
|
+
const meal = MEALS.find((entry) => entry.key === target!.mealType)?.label ?? target.mealType;
|
|
40
|
+
return `${day} · ${meal} · week of ${formatWeekRange(weekKey)}`;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
function openPicker(day: DayKey, mealType: MealKey) {
|
|
44
|
+
target = { day, mealType };
|
|
45
|
+
pickerOpen = true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function handlePick(recipe: RecipeSummary) {
|
|
49
|
+
if (!target) return;
|
|
50
|
+
planner.assign(target.day, target.mealType, recipe, weekKey);
|
|
51
|
+
|
|
52
|
+
const dayLabel = DAYS.find((entry) => entry.key === target!.day)?.label ?? target.day;
|
|
53
|
+
const mealLabel = MEALS.find((entry) => entry.key === target!.mealType)?.label ?? target.mealType;
|
|
54
|
+
toasts.success(`${recipe.name} → ${dayLabel} ${mealLabel.toLowerCase()}.`);
|
|
55
|
+
|
|
56
|
+
pickerOpen = false;
|
|
57
|
+
target = null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* ------------------------------------------------------ slot interactions */
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The component's payload identifies the slot by its display label, so the
|
|
64
|
+
* canonical `DayKey` / `MealKey` are closed over from the grid loop instead.
|
|
65
|
+
*/
|
|
66
|
+
function removeMeal(day: DayKey, mealType: MealKey) {
|
|
67
|
+
const removed = planner.meal(day, mealType, weekKey);
|
|
68
|
+
if (!removed) return;
|
|
69
|
+
|
|
70
|
+
planner.remove(day, mealType, weekKey);
|
|
71
|
+
toasts.push(`Removed ${removed.name} from the plan.`, {
|
|
72
|
+
tone: 'info',
|
|
73
|
+
action: {
|
|
74
|
+
label: 'Undo',
|
|
75
|
+
run: () => planner.assign(day, mealType, { ...removed, id: removed.recipeId }, weekKey)
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Drag payloads are `day:mealType` strings, so a drop can identify the source
|
|
82
|
+
* slot and move the meal rather than copying it.
|
|
83
|
+
*/
|
|
84
|
+
function dropOnto(day: DayKey, mealType: MealKey, payload: string) {
|
|
85
|
+
const [fromDay, fromMeal] = payload.split(':');
|
|
86
|
+
if (!fromDay || !fromMeal) return;
|
|
87
|
+
|
|
88
|
+
planner.move({ day: fromDay as DayKey, mealType: fromMeal as MealKey }, { day, mealType }, weekKey);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function handleDragStart(event: DragEvent, day: DayKey, mealType: MealKey) {
|
|
92
|
+
event.dataTransfer?.setData('text/plain', `${day}:${mealType}`);
|
|
93
|
+
if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* ------------------------------------------------------------ week actions */
|
|
97
|
+
|
|
98
|
+
let confirmClear = $state(false);
|
|
99
|
+
|
|
100
|
+
function clearWeek() {
|
|
101
|
+
const count = plannedCount;
|
|
102
|
+
planner.clearWeek(weekKey);
|
|
103
|
+
confirmClear = false;
|
|
104
|
+
toasts.push(`Cleared ${count} planned ${count === 1 ? 'meal' : 'meals'}.`, { tone: 'info' });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function repeatLastWeek() {
|
|
108
|
+
planner.copyWeek(previousWeekKey, weekKey);
|
|
109
|
+
toasts.success(`Copied ${previousWeekCount} ${previousWeekCount === 1 ? 'meal' : 'meals'} from last week.`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Aggregated ingredient-free shopping overview: recipes and their slot count. */
|
|
113
|
+
let showSummary = $state(true);
|
|
114
|
+
</script>
|
|
115
|
+
|
|
116
|
+
<svelte:head>
|
|
117
|
+
<title>Weekly planner — Mise</title>
|
|
118
|
+
</svelte:head>
|
|
119
|
+
|
|
120
|
+
<div class="page-head">
|
|
121
|
+
<div>
|
|
122
|
+
<p class="eyebrow">Meal planner</p>
|
|
123
|
+
<h1 class="page-title">Week of {formatWeekRange(weekKey)}</h1>
|
|
124
|
+
<p class="page-lede">
|
|
125
|
+
{plannedCount} of {totalSlots} slots planned.
|
|
126
|
+
{#if plannedCount > 0}
|
|
127
|
+
Drag a meal to another slot to move it.
|
|
128
|
+
{:else}
|
|
129
|
+
Pick a slot below to start building the week.
|
|
130
|
+
{/if}
|
|
131
|
+
</p>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<div class="week-nav">
|
|
135
|
+
<button class="btn btn-sm" type="button" onclick={() => (planner.selectedWeek = shiftWeekKey(weekKey, -1))} aria-label="Previous week">
|
|
136
|
+
← Prev
|
|
137
|
+
</button>
|
|
138
|
+
<button class="btn btn-sm" type="button" onclick={() => (planner.selectedWeek = currentWeekKey())} disabled={isCurrentWeek}>
|
|
139
|
+
This week
|
|
140
|
+
</button>
|
|
141
|
+
<button class="btn btn-sm" type="button" onclick={() => (planner.selectedWeek = shiftWeekKey(weekKey, 1))} aria-label="Next week">
|
|
142
|
+
Next →
|
|
143
|
+
</button>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<div class="toolbar">
|
|
148
|
+
{#if previousWeekCount > 0 && plannedCount === 0}
|
|
149
|
+
<button class="btn btn-accent btn-sm" type="button" onclick={repeatLastWeek}>
|
|
150
|
+
Repeat last week ({previousWeekCount} {previousWeekCount === 1 ? 'meal' : 'meals'})
|
|
151
|
+
</button>
|
|
152
|
+
{/if}
|
|
153
|
+
|
|
154
|
+
{#if plannedCount > 0}
|
|
155
|
+
<button class="btn btn-danger btn-sm" type="button" onclick={() => (confirmClear = true)}>Clear this week</button>
|
|
156
|
+
{/if}
|
|
157
|
+
|
|
158
|
+
<a class="btn btn-ghost btn-sm" href="/favorites">Browse favorites</a>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<div class="planner-scroll">
|
|
162
|
+
<div class="planner-grid" style={`--day-count: ${DAYS.length}`}>
|
|
163
|
+
<!-- Header row: one column per day. -->
|
|
164
|
+
<div aria-hidden="true"></div>
|
|
165
|
+
{#each DAYS as day (day.key)}
|
|
166
|
+
{@const date = dateForDay(weekKey, day.key)}
|
|
167
|
+
<div class="day-head" class:is-today={date.toDateString() === today}>
|
|
168
|
+
<span class="day-name">{day.short}</span>
|
|
169
|
+
<span class="day-date">{date.toLocaleDateString(undefined, { day: 'numeric', month: 'short' })}</span>
|
|
170
|
+
</div>
|
|
171
|
+
{/each}
|
|
172
|
+
|
|
173
|
+
<!-- One row per meal type. -->
|
|
174
|
+
{#each MEALS as meal (meal.key)}
|
|
175
|
+
<div class="meal-head">{meal.label}</div>
|
|
176
|
+
|
|
177
|
+
{#each DAYS as day (day.key)}
|
|
178
|
+
{@const planned = planner.meal(day.key, meal.key, weekKey)}
|
|
179
|
+
<div
|
|
180
|
+
class="cell"
|
|
181
|
+
draggable={planned ? 'true' : 'false'}
|
|
182
|
+
ondragstart={(event) => handleDragStart(event, day.key, meal.key)}
|
|
183
|
+
role="presentation"
|
|
184
|
+
>
|
|
185
|
+
<!--
|
|
186
|
+
The planner cell is a Stencil component. Props carry the planned
|
|
187
|
+
recipe down; add / open / remove / drop come back as custom events.
|
|
188
|
+
-->
|
|
189
|
+
<WebComponent
|
|
190
|
+
tag="rk-meal-slot"
|
|
191
|
+
props={{
|
|
192
|
+
day: day.label,
|
|
193
|
+
mealType: meal.key,
|
|
194
|
+
mealLabel: meal.label,
|
|
195
|
+
recipeId: planned?.recipeId,
|
|
196
|
+
recipeName: planned?.name,
|
|
197
|
+
recipeImage: planned?.image
|
|
198
|
+
}}
|
|
199
|
+
events={{
|
|
200
|
+
rkMealAdd: (() => openPicker(day.key, meal.key)) as (event: CustomEvent<never>) => void,
|
|
201
|
+
rkMealSwap: (() => openPicker(day.key, meal.key)) as (event: CustomEvent<never>) => void,
|
|
202
|
+
rkMealOpen: ((event: CustomEvent<{ recipeId: string }>) => goto(`/recipes/${event.detail.recipeId}`)) as (
|
|
203
|
+
event: CustomEvent<never>
|
|
204
|
+
) => void,
|
|
205
|
+
rkMealRemove: (() => removeMeal(day.key, meal.key)) as (event: CustomEvent<never>) => void,
|
|
206
|
+
rkMealDrop: ((event: CustomEvent<{ payload: string }>) => dropOnto(day.key, meal.key, event.detail.payload)) as (
|
|
207
|
+
event: CustomEvent<never>
|
|
208
|
+
) => void
|
|
209
|
+
}}
|
|
210
|
+
>
|
|
211
|
+
{#snippet children()}
|
|
212
|
+
{#if planned?.origin === 'custom'}
|
|
213
|
+
<rk-badge tone="success" small>yours</rk-badge>
|
|
214
|
+
{/if}
|
|
215
|
+
{/snippet}
|
|
216
|
+
</WebComponent>
|
|
217
|
+
</div>
|
|
218
|
+
{/each}
|
|
219
|
+
{/each}
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
{#if plannedCount === 0}
|
|
224
|
+
<div class="section">
|
|
225
|
+
<rk-empty-state
|
|
226
|
+
icon="🗓️"
|
|
227
|
+
heading="Nothing planned for this week yet"
|
|
228
|
+
message="Tap any empty slot to pick a recipe from your favorites, your own collection, or the whole catalogue."
|
|
229
|
+
>
|
|
230
|
+
<a class="btn btn-primary" href="/">Find recipes</a>
|
|
231
|
+
</rk-empty-state>
|
|
232
|
+
</div>
|
|
233
|
+
{:else}
|
|
234
|
+
<section class="section">
|
|
235
|
+
<div class="section-head">
|
|
236
|
+
<h2 class="section-title">This week's recipes</h2>
|
|
237
|
+
<button class="btn btn-ghost btn-sm" type="button" onclick={() => (showSummary = !showSummary)} aria-expanded={showSummary}>
|
|
238
|
+
{showSummary ? 'Hide' : 'Show'}
|
|
239
|
+
</button>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
{#if showSummary}
|
|
243
|
+
<ul class="summary">
|
|
244
|
+
{#each weekRecipes as entry (entry.meal.recipeId)}
|
|
245
|
+
<li class="summary-row card-surface">
|
|
246
|
+
{#if entry.meal.image}
|
|
247
|
+
<img class="summary-thumb" src={entry.meal.image} alt="" width="56" height="56" loading="lazy" />
|
|
248
|
+
{:else}
|
|
249
|
+
<span class="summary-thumb summary-thumb-fallback" aria-hidden="true">🍽️</span>
|
|
250
|
+
{/if}
|
|
251
|
+
|
|
252
|
+
<div class="summary-text">
|
|
253
|
+
<a class="summary-name" href={`/recipes/${entry.meal.recipeId}`}>{entry.meal.name}</a>
|
|
254
|
+
<p class="small muted">{entry.meal.category}{entry.meal.area && entry.meal.area !== 'Unknown' ? ` · ${entry.meal.area}` : ''}</p>
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
<rk-badge tone={entry.slots > 1 ? 'primary' : 'neutral'}>
|
|
258
|
+
{entry.slots} {entry.slots === 1 ? 'slot' : 'slots'}
|
|
259
|
+
</rk-badge>
|
|
260
|
+
</li>
|
|
261
|
+
{/each}
|
|
262
|
+
</ul>
|
|
263
|
+
{/if}
|
|
264
|
+
</section>
|
|
265
|
+
{/if}
|
|
266
|
+
|
|
267
|
+
<RecipePickerDialog
|
|
268
|
+
open={pickerOpen}
|
|
269
|
+
slotLabel={targetLabel}
|
|
270
|
+
onpick={handlePick}
|
|
271
|
+
onclose={() => {
|
|
272
|
+
pickerOpen = false;
|
|
273
|
+
target = null;
|
|
274
|
+
}}
|
|
275
|
+
/>
|
|
276
|
+
|
|
277
|
+
<ConfirmDialog
|
|
278
|
+
open={confirmClear}
|
|
279
|
+
heading="Clear this week?"
|
|
280
|
+
message={`All ${plannedCount} planned ${plannedCount === 1 ? 'meal' : 'meals'} for the week of ${formatWeekRange(weekKey)} will be removed. Other weeks are not affected.`}
|
|
281
|
+
confirmLabel="Clear week"
|
|
282
|
+
destructive
|
|
283
|
+
onconfirm={clearWeek}
|
|
284
|
+
oncancel={() => (confirmClear = false)}
|
|
285
|
+
/>
|
|
286
|
+
|
|
287
|
+
<style>
|
|
288
|
+
.week-nav {
|
|
289
|
+
display: flex;
|
|
290
|
+
gap: 6px;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.toolbar {
|
|
294
|
+
display: flex;
|
|
295
|
+
flex-wrap: wrap;
|
|
296
|
+
gap: 8px;
|
|
297
|
+
margin-bottom: 22px;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* Horizontal scroll keeps all seven days addressable on narrow screens. */
|
|
301
|
+
.planner-scroll {
|
|
302
|
+
overflow-x: auto;
|
|
303
|
+
margin: 0 calc(-1 * var(--app-gutter));
|
|
304
|
+
padding: 2px var(--app-gutter);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.planner-grid {
|
|
308
|
+
display: grid;
|
|
309
|
+
grid-template-columns: 96px repeat(var(--day-count), minmax(148px, 1fr));
|
|
310
|
+
gap: 8px;
|
|
311
|
+
min-width: 860px;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.day-head {
|
|
315
|
+
display: grid;
|
|
316
|
+
gap: 1px;
|
|
317
|
+
padding: 8px 10px;
|
|
318
|
+
border-radius: var(--rk-radius);
|
|
319
|
+
background: var(--rk-surface-2);
|
|
320
|
+
border: 1px solid var(--rk-border);
|
|
321
|
+
text-align: center;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.day-head.is-today {
|
|
325
|
+
background: var(--rk-primary-soft);
|
|
326
|
+
border-color: var(--rk-primary);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.day-name {
|
|
330
|
+
font-weight: 700;
|
|
331
|
+
font-size: 0.85rem;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.day-date {
|
|
335
|
+
font-size: 0.72rem;
|
|
336
|
+
color: var(--rk-muted);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.day-head.is-today .day-date {
|
|
340
|
+
color: var(--rk-primary-strong);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.meal-head {
|
|
344
|
+
display: flex;
|
|
345
|
+
align-items: center;
|
|
346
|
+
font-size: 0.74rem;
|
|
347
|
+
font-weight: 700;
|
|
348
|
+
text-transform: uppercase;
|
|
349
|
+
letter-spacing: 0.07em;
|
|
350
|
+
color: var(--rk-muted);
|
|
351
|
+
padding-right: 8px;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.cell {
|
|
355
|
+
display: flex;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.cell > :global(*) {
|
|
359
|
+
flex: 1 1 auto;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.cell[draggable='true'] {
|
|
363
|
+
cursor: grab;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.cell[draggable='true']:active {
|
|
367
|
+
cursor: grabbing;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.summary {
|
|
371
|
+
list-style: none;
|
|
372
|
+
margin: 0;
|
|
373
|
+
padding: 0;
|
|
374
|
+
display: grid;
|
|
375
|
+
gap: 10px;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.summary-row {
|
|
379
|
+
display: flex;
|
|
380
|
+
align-items: center;
|
|
381
|
+
gap: 14px;
|
|
382
|
+
padding: 12px 16px;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.summary-thumb {
|
|
386
|
+
flex: 0 0 auto;
|
|
387
|
+
width: 56px;
|
|
388
|
+
height: 56px;
|
|
389
|
+
border-radius: var(--rk-radius-sm);
|
|
390
|
+
object-fit: cover;
|
|
391
|
+
background: var(--rk-surface-2);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.summary-thumb-fallback {
|
|
395
|
+
display: grid;
|
|
396
|
+
place-items: center;
|
|
397
|
+
font-size: 1.4rem;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.summary-text {
|
|
401
|
+
flex: 1 1 auto;
|
|
402
|
+
min-width: 0;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.summary-name {
|
|
406
|
+
font-family: var(--rk-font-display);
|
|
407
|
+
font-size: 1.03rem;
|
|
408
|
+
font-weight: 650;
|
|
409
|
+
color: var(--rk-ink);
|
|
410
|
+
text-decoration: none;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.summary-name:hover {
|
|
414
|
+
color: var(--rk-primary);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
@media (max-width: 720px) {
|
|
418
|
+
.page-head {
|
|
419
|
+
align-items: flex-start;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
</style>
|