@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.
Files changed (87) hide show
  1. package/.claude/settings.local.json +10 -0
  2. package/.npmr +1 -0
  3. package/README.md +354 -0
  4. package/apps/recipe-web/package.json +31 -0
  5. package/apps/recipe-web/src/app.css +418 -0
  6. package/apps/recipe-web/src/app.d.ts +32 -0
  7. package/apps/recipe-web/src/app.html +25 -0
  8. package/apps/recipe-web/src/lib/api/mealdb.ts +240 -0
  9. package/apps/recipe-web/src/lib/components/AppHeader.svelte +290 -0
  10. package/apps/recipe-web/src/lib/components/ConfirmDialog.svelte +46 -0
  11. package/apps/recipe-web/src/lib/components/Modal.svelte +33 -0
  12. package/apps/recipe-web/src/lib/components/RecipeCard.svelte +100 -0
  13. package/apps/recipe-web/src/lib/components/RecipeForm.svelte +560 -0
  14. package/apps/recipe-web/src/lib/components/RecipeGrid.svelte +77 -0
  15. package/apps/recipe-web/src/lib/components/RecipePickerDialog.svelte +185 -0
  16. package/apps/recipe-web/src/lib/components/Toaster.svelte +111 -0
  17. package/apps/recipe-web/src/lib/domain/recipe.ts +130 -0
  18. package/apps/recipe-web/src/lib/domain/validation.ts +189 -0
  19. package/apps/recipe-web/src/lib/stencil/WebComponent.svelte +145 -0
  20. package/apps/recipe-web/src/lib/stencil/register.ts +28 -0
  21. package/apps/recipe-web/src/lib/stores/favorites.svelte.ts +81 -0
  22. package/apps/recipe-web/src/lib/stores/myRecipes.svelte.ts +98 -0
  23. package/apps/recipe-web/src/lib/stores/persisted.svelte.ts +73 -0
  24. package/apps/recipe-web/src/lib/stores/planner.svelte.ts +275 -0
  25. package/apps/recipe-web/src/lib/stores/theme.svelte.ts +67 -0
  26. package/apps/recipe-web/src/lib/stores/toast.svelte.ts +66 -0
  27. package/apps/recipe-web/src/routes/+error.svelte +36 -0
  28. package/apps/recipe-web/src/routes/+layout.svelte +62 -0
  29. package/apps/recipe-web/src/routes/+page.svelte +352 -0
  30. package/apps/recipe-web/src/routes/+page.ts +107 -0
  31. package/apps/recipe-web/src/routes/favorites/+page.svelte +170 -0
  32. package/apps/recipe-web/src/routes/my-recipes/+layout.ts +55 -0
  33. package/apps/recipe-web/src/routes/my-recipes/+page.svelte +180 -0
  34. package/apps/recipe-web/src/routes/my-recipes/[id]/edit/+page.svelte +104 -0
  35. package/apps/recipe-web/src/routes/my-recipes/new/+page.svelte +67 -0
  36. package/apps/recipe-web/src/routes/planner/+page.svelte +422 -0
  37. package/apps/recipe-web/src/routes/recipes/[id]/+page.svelte +538 -0
  38. package/apps/recipe-web/src/routes/recipes/[id]/+page.ts +24 -0
  39. package/apps/recipe-web/static/favicon.svg +8 -0
  40. package/apps/recipe-web/static/robots.txt +2 -0
  41. package/apps/recipe-web/svelte.config.js +35 -0
  42. package/apps/recipe-web/tsconfig.json +14 -0
  43. package/apps/recipe-web/vite.config.ts +23 -0
  44. package/package.json +29 -0
  45. package/packages/recipe-kit-stencil/LICENSE +21 -0
  46. package/packages/recipe-kit-stencil/README.md +185 -0
  47. package/packages/recipe-kit-stencil/package-lock.json +4772 -0
  48. package/packages/recipe-kit-stencil/package.json +91 -0
  49. package/packages/recipe-kit-stencil/src/components/rk-badge/readme.md +38 -0
  50. package/packages/recipe-kit-stencil/src/components/rk-badge/rk-badge.css +102 -0
  51. package/packages/recipe-kit-stencil/src/components/rk-badge/rk-badge.tsx +31 -0
  52. package/packages/recipe-kit-stencil/src/components/rk-chip/readme.md +52 -0
  53. package/packages/recipe-kit-stencil/src/components/rk-chip/rk-chip.css +155 -0
  54. package/packages/recipe-kit-stencil/src/components/rk-chip/rk-chip.tsx +105 -0
  55. package/packages/recipe-kit-stencil/src/components/rk-empty-state/readme.md +44 -0
  56. package/packages/recipe-kit-stencil/src/components/rk-empty-state/rk-empty-state.css +64 -0
  57. package/packages/recipe-kit-stencil/src/components/rk-empty-state/rk-empty-state.tsx +50 -0
  58. package/packages/recipe-kit-stencil/src/components/rk-meal-slot/readme.md +58 -0
  59. package/packages/recipe-kit-stencil/src/components/rk-meal-slot/rk-meal-slot.css +225 -0
  60. package/packages/recipe-kit-stencil/src/components/rk-meal-slot/rk-meal-slot.tsx +182 -0
  61. package/packages/recipe-kit-stencil/src/components/rk-modal/readme.md +57 -0
  62. package/packages/recipe-kit-stencil/src/components/rk-modal/rk-modal.css +163 -0
  63. package/packages/recipe-kit-stencil/src/components/rk-modal/rk-modal.tsx +176 -0
  64. package/packages/recipe-kit-stencil/src/components/rk-rating/readme.md +42 -0
  65. package/packages/recipe-kit-stencil/src/components/rk-rating/rk-rating.css +71 -0
  66. package/packages/recipe-kit-stencil/src/components/rk-rating/rk-rating.tsx +114 -0
  67. package/packages/recipe-kit-stencil/src/components/rk-recipe-card/readme.md +66 -0
  68. package/packages/recipe-kit-stencil/src/components/rk-recipe-card/rk-recipe-card.css +280 -0
  69. package/packages/recipe-kit-stencil/src/components/rk-recipe-card/rk-recipe-card.tsx +179 -0
  70. package/packages/recipe-kit-stencil/src/components/rk-recipe-card/test/rk-recipe-card.spec.tsx +70 -0
  71. package/packages/recipe-kit-stencil/src/components/rk-search-bar/readme.md +67 -0
  72. package/packages/recipe-kit-stencil/src/components/rk-search-bar/rk-search-bar.css +141 -0
  73. package/packages/recipe-kit-stencil/src/components/rk-search-bar/rk-search-bar.tsx +152 -0
  74. package/packages/recipe-kit-stencil/src/components/rk-search-bar/test/rk-search-bar.spec.tsx +93 -0
  75. package/packages/recipe-kit-stencil/src/components/rk-segmented-control/readme.md +41 -0
  76. package/packages/recipe-kit-stencil/src/components/rk-segmented-control/rk-segmented-control.css +91 -0
  77. package/packages/recipe-kit-stencil/src/components/rk-segmented-control/rk-segmented-control.tsx +115 -0
  78. package/packages/recipe-kit-stencil/src/components/rk-tag-input/readme.md +47 -0
  79. package/packages/recipe-kit-stencil/src/components/rk-tag-input/rk-tag-input.css +146 -0
  80. package/packages/recipe-kit-stencil/src/components/rk-tag-input/rk-tag-input.tsx +181 -0
  81. package/packages/recipe-kit-stencil/src/components/rk-tag-input/test/rk-tag-input.spec.tsx +76 -0
  82. package/packages/recipe-kit-stencil/src/components.d.ts +1192 -0
  83. package/packages/recipe-kit-stencil/src/global/rk-global.css +104 -0
  84. package/packages/recipe-kit-stencil/src/index.html +259 -0
  85. package/packages/recipe-kit-stencil/src/index.ts +6 -0
  86. package/packages/recipe-kit-stencil/stencil.config.ts +41 -0
  87. package/packages/recipe-kit-stencil/tsconfig.json +24 -0
@@ -0,0 +1,50 @@
1
+ import { Component, h, Prop } from '@stencil/core';
2
+
3
+ /**
4
+ * Placeholder shown when a list has no items, a search returns nothing, or a
5
+ * request fails.
6
+ *
7
+ * @slot - Call-to-action content rendered under the message (buttons, links).
8
+ */
9
+ @Component({
10
+ tag: 'rk-empty-state',
11
+ styleUrl: 'rk-empty-state.css',
12
+ shadow: true,
13
+ })
14
+ export class RkEmptyState {
15
+ /** Emoji or short glyph shown above the heading. */
16
+ @Prop() icon = '🍳';
17
+
18
+ /** Bold headline. */
19
+ @Prop() heading = 'Nothing here yet';
20
+
21
+ /** Supporting sentence. */
22
+ @Prop() message?: string;
23
+
24
+ /** `error` swaps to a warning treatment. */
25
+ @Prop() tone: 'neutral' | 'error' = 'neutral';
26
+
27
+ /** Removes the dashed border and reduces padding, for inline use. */
28
+ @Prop() bare = false;
29
+
30
+ render() {
31
+ return (
32
+ <div class={{ empty: true, [`tone-${this.tone}`]: true, 'is-bare': this.bare }} part="container" role={this.tone === 'error' ? 'alert' : null}>
33
+ <span class="icon" part="icon" aria-hidden="true">
34
+ {this.icon}
35
+ </span>
36
+ <h3 class="heading" part="heading">
37
+ {this.heading}
38
+ </h3>
39
+ {this.message && (
40
+ <p class="message" part="message">
41
+ {this.message}
42
+ </p>
43
+ )}
44
+ <div class="actions" part="actions">
45
+ <slot />
46
+ </div>
47
+ </div>
48
+ );
49
+ }
50
+ }
@@ -0,0 +1,58 @@
1
+ # rk-meal-slot
2
+
3
+
4
+
5
+ <!-- Auto Generated Below -->
6
+
7
+
8
+ ## Overview
9
+
10
+ One cell of the weekly planner grid: either an empty "add" target or a filled
11
+ slot showing the planned recipe. Doubles as a drop zone for drag-and-drop
12
+ planning.
13
+
14
+ ## Properties
15
+
16
+ | Property | Attribute | Description | Type | Default |
17
+ | ----------------------- | -------------- | ---------------------------------------------------------------------- | --------- | ----------- |
18
+ | `day` _(required)_ | `day` | Day key this slot belongs to, e.g. `monday`. Echoed in event payloads. | `string` | `undefined` |
19
+ | `disabled` | `disabled` | Disables interaction. | `boolean` | `false` |
20
+ | `mealLabel` | `meal-label` | Human-readable meal label shown as the slot caption. | `string` | `undefined` |
21
+ | `mealType` _(required)_ | `meal-type` | Meal type key, e.g. `breakfast`. Echoed in event payloads. | `string` | `undefined` |
22
+ | `recipeId` | `recipe-id` | Id of the planned recipe. Empty means the slot is free. | `string` | `undefined` |
23
+ | `recipeImage` | `recipe-image` | Thumbnail of the planned recipe. | `string` | `undefined` |
24
+ | `recipeName` | `recipe-name` | Title of the planned recipe. | `string` | `undefined` |
25
+
26
+
27
+ ## Events
28
+
29
+ | Event | Description | Type |
30
+ | -------------- | ------------------------------------------------------------------------- | --------------------------------- |
31
+ | `rkMealAdd` | Emitted when an empty slot is activated — the host opens a recipe picker. | `CustomEvent<RkMealSlotDetail>` |
32
+ | `rkMealDrop` | Emitted when a recipe is dropped onto this slot. | `CustomEvent<RkMealDropDetail>` |
33
+ | `rkMealOpen` | Emitted when the planned recipe is activated — the host navigates. | `CustomEvent<RkMealRecipeDetail>` |
34
+ | `rkMealRemove` | Emitted when the planned recipe should be cleared. | `CustomEvent<RkMealRecipeDetail>` |
35
+ | `rkMealSwap` | Emitted when the planned recipe should be replaced. | `CustomEvent<RkMealRecipeDetail>` |
36
+
37
+
38
+ ## Slots
39
+
40
+ | Slot | Description |
41
+ | ---- | ------------------------------------------------------------------ |
42
+ | | Extra content rendered under the recipe name (e.g. serving notes). |
43
+
44
+
45
+ ## Shadow Parts
46
+
47
+ | Part | Description |
48
+ | ------------ | ----------- |
49
+ | `"caption"` | |
50
+ | `"controls"` | |
51
+ | `"main"` | |
52
+ | `"name"` | |
53
+ | `"slot"` | |
54
+
55
+
56
+ ----------------------------------------------
57
+
58
+ *Built with [StencilJS](https://stenciljs.com/)*
@@ -0,0 +1,225 @@
1
+ :host {
2
+ display: block;
3
+ font-family: var(--rk-font);
4
+ font-size: var(--rk-font-size, 15px);
5
+ color: var(--rk-ink, #211a16);
6
+ }
7
+
8
+ .slot {
9
+ position: relative;
10
+ width: 100%;
11
+ box-sizing: border-box;
12
+ border-radius: var(--rk-radius, 14px);
13
+ transition: border-color var(--rk-transition), background var(--rk-transition), box-shadow var(--rk-transition);
14
+ }
15
+
16
+ /* ---------- empty ---------- */
17
+
18
+ .slot.is-empty {
19
+ display: flex;
20
+ flex-direction: column;
21
+ align-items: center;
22
+ justify-content: center;
23
+ gap: 3px;
24
+ min-height: 86px;
25
+ padding: 12px 10px;
26
+ border: 1.5px dashed var(--rk-border-strong, #d9c6b8);
27
+ background: transparent;
28
+ color: var(--rk-muted, #857166);
29
+ font: inherit;
30
+ cursor: pointer;
31
+ }
32
+
33
+ .slot.is-empty:hover:not(:disabled) {
34
+ border-color: var(--rk-primary, #d94f1b);
35
+ background: var(--rk-primary-soft, #fdeee6);
36
+ color: var(--rk-primary-strong, #b23c11);
37
+ }
38
+
39
+ .slot.is-empty:focus-visible {
40
+ outline: none;
41
+ box-shadow: var(--rk-focus-ring);
42
+ }
43
+
44
+ .slot.is-empty:disabled {
45
+ opacity: 0.5;
46
+ cursor: not-allowed;
47
+ }
48
+
49
+ .plus {
50
+ font-size: 1.3rem;
51
+ line-height: 1;
52
+ font-weight: 300;
53
+ }
54
+
55
+ .empty-label {
56
+ font-size: 0.76em;
57
+ font-weight: 600;
58
+ text-transform: uppercase;
59
+ letter-spacing: 0.06em;
60
+ }
61
+
62
+ /* ---------- filled ---------- */
63
+
64
+ .slot.is-filled {
65
+ display: flex;
66
+ flex-direction: column;
67
+ gap: 6px;
68
+ min-height: 86px;
69
+ padding: 10px;
70
+ border: 1px solid var(--rk-border, #ecdfd4);
71
+ background: var(--rk-surface, #fff);
72
+ box-shadow: var(--rk-shadow-sm);
73
+ }
74
+
75
+ .caption {
76
+ font-size: 0.7em;
77
+ font-weight: 700;
78
+ text-transform: uppercase;
79
+ letter-spacing: 0.07em;
80
+ color: var(--rk-muted, #857166);
81
+ }
82
+
83
+ .main {
84
+ display: flex;
85
+ align-items: center;
86
+ gap: 9px;
87
+ width: 100%;
88
+ padding: 0;
89
+ border: 0;
90
+ background: transparent;
91
+ text-align: left;
92
+ font: inherit;
93
+ color: inherit;
94
+ cursor: pointer;
95
+ border-radius: var(--rk-radius-sm, 8px);
96
+ }
97
+
98
+ .main:hover:not(:disabled) .name {
99
+ color: var(--rk-primary, #d94f1b);
100
+ }
101
+
102
+ .main:focus-visible {
103
+ outline: none;
104
+ box-shadow: var(--rk-focus-ring);
105
+ }
106
+
107
+ .main:disabled {
108
+ cursor: not-allowed;
109
+ }
110
+
111
+ .thumb {
112
+ flex: 0 0 auto;
113
+ width: 38px;
114
+ height: 38px;
115
+ border-radius: var(--rk-radius-sm, 8px);
116
+ object-fit: cover;
117
+ background: var(--rk-surface-2, #fdf5ef);
118
+ }
119
+
120
+ .thumb-fallback {
121
+ display: grid;
122
+ place-items: center;
123
+ font-size: 1.1rem;
124
+ }
125
+
126
+ .name {
127
+ font-size: 0.87em;
128
+ font-weight: 600;
129
+ line-height: 1.3;
130
+ display: -webkit-box;
131
+ -webkit-line-clamp: 2;
132
+ -webkit-box-orient: vertical;
133
+ overflow: hidden;
134
+ transition: color var(--rk-transition);
135
+ }
136
+
137
+ .extra {
138
+ display: contents;
139
+ }
140
+
141
+ .extra ::slotted(*) {
142
+ font-size: 0.8em;
143
+ color: var(--rk-muted, #857166);
144
+ }
145
+
146
+ .controls {
147
+ position: absolute;
148
+ top: 6px;
149
+ right: 6px;
150
+ display: flex;
151
+ gap: 3px;
152
+ opacity: 0;
153
+ transition: opacity var(--rk-transition);
154
+ }
155
+
156
+ .slot.is-filled:hover .controls,
157
+ .slot.is-filled:focus-within .controls {
158
+ opacity: 1;
159
+ }
160
+
161
+ .ctrl {
162
+ display: inline-flex;
163
+ align-items: center;
164
+ justify-content: center;
165
+ width: 24px;
166
+ height: 24px;
167
+ padding: 0;
168
+ border: 1px solid var(--rk-border, #ecdfd4);
169
+ border-radius: 50%;
170
+ background: var(--rk-surface, #fff);
171
+ color: var(--rk-ink-2, #52443c);
172
+ cursor: pointer;
173
+ box-shadow: var(--rk-shadow-sm);
174
+ transition: background var(--rk-transition), color var(--rk-transition);
175
+ }
176
+
177
+ .ctrl:hover:not(:disabled) {
178
+ background: var(--rk-surface-2, #fdf5ef);
179
+ color: var(--rk-primary, #d94f1b);
180
+ }
181
+
182
+ .ctrl.is-danger:hover:not(:disabled) {
183
+ background: var(--rk-danger-soft, #fdeae1);
184
+ color: var(--rk-danger, #c2410c);
185
+ }
186
+
187
+ .ctrl:focus-visible {
188
+ outline: none;
189
+ box-shadow: var(--rk-focus-ring);
190
+ }
191
+
192
+ .ctrl:disabled {
193
+ opacity: 0.5;
194
+ cursor: not-allowed;
195
+ }
196
+
197
+ .ctrl svg {
198
+ width: 12px;
199
+ height: 12px;
200
+ }
201
+
202
+ /* ---------- drag & drop ---------- */
203
+
204
+ .slot.is-drag-over {
205
+ border-color: var(--rk-accent, #0f766e) !important;
206
+ border-style: solid;
207
+ background: var(--rk-accent-soft, #e2f3f1) !important;
208
+ box-shadow: 0 0 0 3px rgba(15, 118, 110, 0.2);
209
+ }
210
+
211
+ /* Touch devices have no hover, so the controls stay visible. */
212
+ @media (hover: none) {
213
+ .controls {
214
+ opacity: 1;
215
+ }
216
+ }
217
+
218
+ @media (prefers-reduced-motion: reduce) {
219
+ .slot,
220
+ .controls,
221
+ .ctrl,
222
+ .name {
223
+ transition: none;
224
+ }
225
+ }
@@ -0,0 +1,182 @@
1
+ import { Component, Event, EventEmitter, h, Prop, State } from '@stencil/core';
2
+
3
+ export interface RkMealSlotDetail {
4
+ day: string;
5
+ mealType: string;
6
+ }
7
+
8
+ export interface RkMealRecipeDetail extends RkMealSlotDetail {
9
+ recipeId: string;
10
+ recipeName: string;
11
+ }
12
+
13
+ export interface RkMealDropDetail extends RkMealSlotDetail {
14
+ /** Raw `text/plain` payload from the drag operation. */
15
+ payload: string;
16
+ }
17
+
18
+ /**
19
+ * One cell of the weekly planner grid: either an empty "add" target or a filled
20
+ * slot showing the planned recipe. Doubles as a drop zone for drag-and-drop
21
+ * planning.
22
+ *
23
+ * @slot - Extra content rendered under the recipe name (e.g. serving notes).
24
+ */
25
+ @Component({
26
+ tag: 'rk-meal-slot',
27
+ styleUrl: 'rk-meal-slot.css',
28
+ shadow: true,
29
+ })
30
+ export class RkMealSlot {
31
+ /** Day key this slot belongs to, e.g. `monday`. Echoed in event payloads. */
32
+ @Prop() day!: string;
33
+
34
+ /** Meal type key, e.g. `breakfast`. Echoed in event payloads. */
35
+ @Prop() mealType!: string;
36
+
37
+ /** Human-readable meal label shown as the slot caption. */
38
+ @Prop() mealLabel?: string;
39
+
40
+ /** Id of the planned recipe. Empty means the slot is free. */
41
+ @Prop() recipeId?: string;
42
+
43
+ /** Title of the planned recipe. */
44
+ @Prop() recipeName?: string;
45
+
46
+ /** Thumbnail of the planned recipe. */
47
+ @Prop() recipeImage?: string;
48
+
49
+ /** Disables interaction. */
50
+ @Prop() disabled = false;
51
+
52
+ /** Emitted when an empty slot is activated — the host opens a recipe picker. */
53
+ @Event({ eventName: 'rkMealAdd' }) rkMealAdd!: EventEmitter<RkMealSlotDetail>;
54
+
55
+ /** Emitted when the planned recipe should be replaced. */
56
+ @Event({ eventName: 'rkMealSwap' }) rkMealSwap!: EventEmitter<RkMealRecipeDetail>;
57
+
58
+ /** Emitted when the planned recipe should be cleared. */
59
+ @Event({ eventName: 'rkMealRemove' }) rkMealRemove!: EventEmitter<RkMealRecipeDetail>;
60
+
61
+ /** Emitted when the planned recipe is activated — the host navigates. */
62
+ @Event({ eventName: 'rkMealOpen' }) rkMealOpen!: EventEmitter<RkMealRecipeDetail>;
63
+
64
+ /** Emitted when a recipe is dropped onto this slot. */
65
+ @Event({ eventName: 'rkMealDrop' }) rkMealDrop!: EventEmitter<RkMealDropDetail>;
66
+
67
+ @State() private dragOver = false;
68
+
69
+ private get filled() {
70
+ return !!this.recipeId;
71
+ }
72
+
73
+ private get slotDetail(): RkMealSlotDetail {
74
+ return { day: this.day, mealType: this.mealType };
75
+ }
76
+
77
+ private get recipeDetail(): RkMealRecipeDetail {
78
+ return { ...this.slotDetail, recipeId: this.recipeId ?? '', recipeName: this.recipeName ?? '' };
79
+ }
80
+
81
+ private handleDragOver = (event: DragEvent) => {
82
+ if (this.disabled) return;
83
+ // Preventing default is what marks this element as a valid drop target.
84
+ event.preventDefault();
85
+ if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy';
86
+ this.dragOver = true;
87
+ };
88
+
89
+ private handleDrop = (event: DragEvent) => {
90
+ event.preventDefault();
91
+ this.dragOver = false;
92
+ if (this.disabled) return;
93
+ const payload = event.dataTransfer?.getData('text/plain') ?? '';
94
+ if (payload) this.rkMealDrop.emit({ ...this.slotDetail, payload });
95
+ };
96
+
97
+ render() {
98
+ if (!this.filled) {
99
+ return (
100
+ <button
101
+ class={{ slot: true, 'is-empty': true, 'is-drag-over': this.dragOver }}
102
+ part="slot"
103
+ type="button"
104
+ disabled={this.disabled}
105
+ aria-label={`Add a recipe for ${this.mealLabel ?? this.mealType} on ${this.day}`}
106
+ onClick={() => this.rkMealAdd.emit(this.slotDetail)}
107
+ onDragOver={this.handleDragOver}
108
+ onDragLeave={() => (this.dragOver = false)}
109
+ onDrop={this.handleDrop}
110
+ >
111
+ <span class="plus" aria-hidden="true">
112
+ +
113
+ </span>
114
+ <span class="empty-label">{this.mealLabel ?? this.mealType}</span>
115
+ </button>
116
+ );
117
+ }
118
+
119
+ return (
120
+ <div
121
+ class={{ slot: true, 'is-filled': true, 'is-drag-over': this.dragOver }}
122
+ part="slot"
123
+ onDragOver={this.handleDragOver}
124
+ onDragLeave={() => (this.dragOver = false)}
125
+ onDrop={this.handleDrop}
126
+ >
127
+ {this.mealLabel && (
128
+ <span class="caption" part="caption">
129
+ {this.mealLabel}
130
+ </span>
131
+ )}
132
+
133
+ <button
134
+ class="main"
135
+ part="main"
136
+ type="button"
137
+ disabled={this.disabled}
138
+ title={this.recipeName}
139
+ onClick={() => this.rkMealOpen.emit(this.recipeDetail)}
140
+ >
141
+ {this.recipeImage ? (
142
+ <img class="thumb" src={this.recipeImage} alt="" loading="lazy" decoding="async" />
143
+ ) : (
144
+ <span class="thumb thumb-fallback" aria-hidden="true">
145
+ 🍽️
146
+ </span>
147
+ )}
148
+ <span class="name" part="name">
149
+ {this.recipeName}
150
+ </span>
151
+ </button>
152
+
153
+ <div class="extra">
154
+ <slot />
155
+ </div>
156
+
157
+ <div class="controls" part="controls">
158
+ <button class="ctrl" type="button" disabled={this.disabled} aria-label={`Replace ${this.recipeName}`} title="Replace" onClick={() => this.rkMealSwap.emit(this.recipeDetail)}>
159
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
160
+ <path d="M3 12a9 9 0 0 1 15.3-6.4L21 8" />
161
+ <path d="M21 3v5h-5" />
162
+ <path d="M21 12a9 9 0 0 1-15.3 6.4L3 16" />
163
+ <path d="M3 21v-5h5" />
164
+ </svg>
165
+ </button>
166
+ <button
167
+ class="ctrl is-danger"
168
+ type="button"
169
+ disabled={this.disabled}
170
+ aria-label={`Remove ${this.recipeName} from ${this.mealLabel ?? this.mealType}`}
171
+ title="Remove"
172
+ onClick={() => this.rkMealRemove.emit(this.recipeDetail)}
173
+ >
174
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
175
+ <path d="M6 6l12 12M18 6 6 18" />
176
+ </svg>
177
+ </button>
178
+ </div>
179
+ </div>
180
+ );
181
+ }
182
+ }
@@ -0,0 +1,57 @@
1
+ # rk-modal
2
+
3
+
4
+
5
+ <!-- Auto Generated Below -->
6
+
7
+
8
+ ## Overview
9
+
10
+ An accessible dialog with a backdrop, Escape handling and focus restoration.
11
+
12
+ The component is presentational: it emits `rkModalClose` and lets the host
13
+ application own the `open` state.
14
+
15
+ ## Properties
16
+
17
+ | Property | Attribute | Description | Type | Default |
18
+ | ------------- | ------------- | ------------------------------------------------------ | ---------------------- | ----------- |
19
+ | `dismissible` | `dismissible` | Set to false to require an explicit action to dismiss. | `boolean` | `true` |
20
+ | `heading` | `heading` | Heading text for the default header. | `string` | `''` |
21
+ | `open` | `open` | Controls visibility. | `boolean` | `false` |
22
+ | `size` | `size` | Dialog width preset. | `"lg" \| "md" \| "sm"` | `'md'` |
23
+ | `subheading` | `subheading` | Optional supporting line under the heading. | `string` | `undefined` |
24
+
25
+
26
+ ## Events
27
+
28
+ | Event | Description | Type |
29
+ | -------------- | ----------------------------------------------------------------- | ------------------- |
30
+ | `rkModalClose` | Emitted when the user requests dismissal (X, backdrop or Escape). | `CustomEvent<void>` |
31
+
32
+
33
+ ## Slots
34
+
35
+ | Slot | Description |
36
+ | ---------- | ---------------------------------------------- |
37
+ | | Dialog body. |
38
+ | `"footer"` | Action row pinned to the bottom of the dialog. |
39
+ | `"header"` | Replaces the default heading row entirely. |
40
+
41
+
42
+ ## Shadow Parts
43
+
44
+ | Part | Description |
45
+ | ------------ | ----------- |
46
+ | `"backdrop"` | |
47
+ | `"body"` | |
48
+ | `"close"` | |
49
+ | `"footer"` | |
50
+ | `"header"` | |
51
+ | `"heading"` | |
52
+ | `"panel"` | |
53
+
54
+
55
+ ----------------------------------------------
56
+
57
+ *Built with [StencilJS](https://stenciljs.com/)*