recipe-planner-ui 1.0.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/LICENSE +21 -0
- package/dist/cjs/app-globals-V2Kpy_OQ.js +5 -0
- package/dist/cjs/index-B6uIqnrk.js +2043 -0
- package/dist/cjs/index.cjs.js +2 -0
- package/dist/cjs/loader.cjs.js +13 -0
- package/dist/cjs/recipe-planner-ui.cjs.js +25 -0
- package/dist/cjs/rp-day-slot.cjs.entry.js +57 -0
- package/dist/cjs/rp-filter-chips_5.cjs.entry.js +240 -0
- package/dist/collection/collection-manifest.json +18 -0
- package/dist/collection/components/rp-day-slot/rp-day-slot.css +131 -0
- package/dist/collection/components/rp-day-slot/rp-day-slot.js +201 -0
- package/dist/collection/components/rp-filter-chips/rp-filter-chips.css +36 -0
- package/dist/collection/components/rp-filter-chips/rp-filter-chips.js +123 -0
- package/dist/collection/components/rp-ingredient-list/rp-ingredient-list.css +53 -0
- package/dist/collection/components/rp-ingredient-list/rp-ingredient-list.js +57 -0
- package/dist/collection/components/rp-modal/rp-modal.css +94 -0
- package/dist/collection/components/rp-modal/rp-modal.js +196 -0
- package/dist/collection/components/rp-recipe-card/rp-recipe-card.css +106 -0
- package/dist/collection/components/rp-recipe-card/rp-recipe-card.js +170 -0
- package/dist/collection/components/rp-search-bar/rp-search-bar.css +65 -0
- package/dist/collection/components/rp-search-bar/rp-search-bar.js +166 -0
- package/dist/collection/index.js +1 -0
- package/dist/components/index.d.ts +35 -0
- package/dist/components/index.js +1 -0
- package/dist/components/rp-day-slot.d.ts +11 -0
- package/dist/components/rp-day-slot.js +1 -0
- package/dist/components/rp-filter-chips.d.ts +11 -0
- package/dist/components/rp-filter-chips.js +1 -0
- package/dist/components/rp-ingredient-list.d.ts +11 -0
- package/dist/components/rp-ingredient-list.js +1 -0
- package/dist/components/rp-modal.d.ts +11 -0
- package/dist/components/rp-modal.js +1 -0
- package/dist/components/rp-recipe-card.d.ts +11 -0
- package/dist/components/rp-recipe-card.js +1 -0
- package/dist/components/rp-search-bar.d.ts +11 -0
- package/dist/components/rp-search-bar.js +1 -0
- package/dist/esm/app-globals-DQuL1Twl.js +3 -0
- package/dist/esm/index-B4wAFfci.js +2034 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/loader.js +11 -0
- package/dist/esm/recipe-planner-ui.js +21 -0
- package/dist/esm/rp-day-slot.entry.js +55 -0
- package/dist/esm/rp-filter-chips_5.entry.js +234 -0
- package/dist/index.cjs.js +1 -0
- package/dist/index.js +1 -0
- package/dist/recipe-planner-ui/index.esm.js +0 -0
- package/dist/recipe-planner-ui/p-66ba2983.entry.js +1 -0
- package/dist/recipe-planner-ui/p-B4wAFfci.js +3 -0
- package/dist/recipe-planner-ui/p-DQuL1Twl.js +1 -0
- package/dist/recipe-planner-ui/p-d54dbd37.entry.js +1 -0
- package/dist/recipe-planner-ui/recipe-planner-ui.css +1 -0
- package/dist/recipe-planner-ui/recipe-planner-ui.esm.js +1 -0
- package/dist/types/components/rp-day-slot/rp-day-slot.d.ts +41 -0
- package/dist/types/components/rp-filter-chips/rp-filter-chips.d.ts +24 -0
- package/dist/types/components/rp-ingredient-list/rp-ingredient-list.d.ts +14 -0
- package/dist/types/components/rp-modal/rp-modal.d.ts +34 -0
- package/dist/types/components/rp-recipe-card/rp-recipe-card.d.ts +36 -0
- package/dist/types/components/rp-search-bar/rp-search-bar.d.ts +31 -0
- package/dist/types/components.d.ts +536 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/stencil-public-runtime.d.ts +1873 -0
- package/loader/cdn.js +1 -0
- package/loader/index.cjs.js +1 -0
- package/loader/index.d.ts +24 -0
- package/loader/index.es2017.js +1 -0
- package/loader/index.js +2 -0
- package/package.json +72 -0
- package/readme.md +131 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var index = require('./index-B6uIqnrk.js');
|
|
4
|
+
var appGlobals = require('./app-globals-V2Kpy_OQ.js');
|
|
5
|
+
|
|
6
|
+
const defineCustomElements = async (win, options) => {
|
|
7
|
+
if (typeof window === 'undefined') return undefined;
|
|
8
|
+
await appGlobals.globalScripts();
|
|
9
|
+
return index.bootstrapLazy([["rp-filter-chips_5.cjs",[[514,"rp-filter-chips",{"options":[16],"selected":[1],"label":[1]}],[774,"rp-ingredient-list",{"items":[16]}],[774,"rp-modal",{"open":[516],"heading":[1],"focusFirstField":[64]},null,{"open":[{"onOpenChange":0}]}],[774,"rp-recipe-card",{"recipeId":[1,"recipe-id"],"recipeTitle":[1,"recipe-title"],"image":[1],"category":[1],"favorite":[516]}],[514,"rp-search-bar",{"value":[1],"placeholder":[1],"label":[1],"draft":[32]},null,{"value":[{"onValueChange":0}]}]]],["rp-day-slot.cjs",[[774,"rp-day-slot",{"day":[1],"dayLabel":[1,"day-label"],"meals":[16],"days":[16],"normalized":[32]}]]]], options);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
exports.setNonce = index.setNonce;
|
|
13
|
+
exports.defineCustomElements = defineCustomElements;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var index = require('./index-B6uIqnrk.js');
|
|
4
|
+
var appGlobals = require('./app-globals-V2Kpy_OQ.js');
|
|
5
|
+
|
|
6
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
7
|
+
/*
|
|
8
|
+
Stencil Client Patch Browser v4.44.1 | MIT Licensed | https://stenciljs.com
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
var patchBrowser = () => {
|
|
12
|
+
const importMeta = (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('recipe-planner-ui.cjs.js', document.baseURI).href));
|
|
13
|
+
const opts = {};
|
|
14
|
+
if (importMeta !== "") {
|
|
15
|
+
opts.resourcesUrl = new URL(".", importMeta).href;
|
|
16
|
+
}
|
|
17
|
+
return index.promiseResolve(opts);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
patchBrowser().then(async (options) => {
|
|
21
|
+
await appGlobals.globalScripts();
|
|
22
|
+
return index.bootstrapLazy([["rp-filter-chips_5.cjs",[[514,"rp-filter-chips",{"options":[16],"selected":[1],"label":[1]}],[774,"rp-ingredient-list",{"items":[16]}],[774,"rp-modal",{"open":[516],"heading":[1],"focusFirstField":[64]},null,{"open":[{"onOpenChange":0}]}],[774,"rp-recipe-card",{"recipeId":[1,"recipe-id"],"recipeTitle":[1,"recipe-title"],"image":[1],"category":[1],"favorite":[516]}],[514,"rp-search-bar",{"value":[1],"placeholder":[1],"label":[1],"draft":[32]},null,{"value":[{"onValueChange":0}]}]]],["rp-day-slot.cjs",[[774,"rp-day-slot",{"day":[1],"dayLabel":[1,"day-label"],"meals":[16],"days":[16],"normalized":[32]}]]]], options);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
exports.setNonce = index.setNonce;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var index = require('./index-B6uIqnrk.js');
|
|
4
|
+
|
|
5
|
+
const rpDaySlotCss = () => `.sc-rp-day-slot-h{display:block;font-family:var(--rp-font-sans);color:var(--rp-color-text)}.slot.sc-rp-day-slot{display:flex;flex-direction:column;height:100%;min-height:160px;background:var(--rp-color-surface);border:1px solid var(--rp-color-border);border-radius:var(--rp-radius-md)}.head.sc-rp-day-slot{display:flex;gap:var(--rp-space-2);align-items:center;justify-content:space-between;padding:var(--rp-space-2) var(--rp-space-3);border-bottom:1px solid var(--rp-color-border)}.day.sc-rp-day-slot{margin:0;font-size:var(--rp-font-size-md);text-transform:capitalize}.add.sc-rp-day-slot{display:grid;place-items:center;width:28px;height:28px;padding:0;font-size:1.125rem;line-height:1;color:var(--rp-color-accent-contrast);cursor:pointer;background:var(--rp-color-accent);border:none;border-radius:50%}.meals.sc-rp-day-slot{flex:1;margin:0;padding:var(--rp-space-2);list-style:none;display:flex;flex-direction:column;gap:var(--rp-space-2)}.meal.sc-rp-day-slot{padding:var(--rp-space-2);background:var(--rp-color-surface-sunken);border-radius:var(--rp-radius-sm)}.meal-title.sc-rp-day-slot{display:block;font-size:var(--rp-font-size-sm);line-height:1.35}.meal-actions.sc-rp-day-slot{display:flex;gap:var(--rp-space-1);align-items:center;justify-content:space-between;margin-top:var(--rp-space-2)}.move-select.sc-rp-day-slot{max-width:100%;padding:2px var(--rp-space-1);font:inherit;font-size:var(--rp-font-size-sm);color:var(--rp-color-text-muted);background:var(--rp-color-surface);border:1px solid var(--rp-color-border);border-radius:var(--rp-radius-sm);text-transform:capitalize}.remove.sc-rp-day-slot{display:grid;place-items:center;width:24px;height:24px;padding:0;font-size:1.125rem;line-height:1;color:var(--rp-color-danger);cursor:pointer;background:none;border:none;border-radius:var(--rp-radius-sm)}.empty.sc-rp-day-slot{display:grid;flex:1;place-items:center;padding:var(--rp-space-3)}.empty-text.sc-rp-day-slot{margin:0;font-size:var(--rp-font-size-sm);color:var(--rp-color-text-muted)}.add.sc-rp-day-slot:focus-visible,.remove.sc-rp-day-slot:focus-visible,.move-select.sc-rp-day-slot:focus-visible{outline:var(--rp-focus-ring);outline-offset:var(--rp-focus-offset)}.sr-only.sc-rp-day-slot{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}`;
|
|
6
|
+
|
|
7
|
+
const DaySlot = class {
|
|
8
|
+
constructor(hostRef) {
|
|
9
|
+
index.registerInstance(this, hostRef);
|
|
10
|
+
this.rpAddMeal = index.createEvent(this, "rpAddMeal");
|
|
11
|
+
this.rpRemoveMeal = index.createEvent(this, "rpRemoveMeal");
|
|
12
|
+
this.rpMoveMeal = index.createEvent(this, "rpMoveMeal");
|
|
13
|
+
}
|
|
14
|
+
/** Machine-readable day key, echoed in every event. */
|
|
15
|
+
day;
|
|
16
|
+
/** Human-readable day name shown as the column heading. */
|
|
17
|
+
dayLabel = '';
|
|
18
|
+
/** Meals planned for this day. Must be assigned as a DOM property. */
|
|
19
|
+
meals = [];
|
|
20
|
+
/** All day keys, used to populate each meal's move target list. */
|
|
21
|
+
days = [];
|
|
22
|
+
normalized = [];
|
|
23
|
+
/** Fired when the user asks to add a meal to this day. */
|
|
24
|
+
rpAddMeal;
|
|
25
|
+
/** Fired when a meal is removed from this day. */
|
|
26
|
+
rpRemoveMeal;
|
|
27
|
+
/** Fired when a meal is reassigned to a different day. */
|
|
28
|
+
rpMoveMeal;
|
|
29
|
+
componentWillLoad() {
|
|
30
|
+
this.normalized = Array.isArray(this.meals) ? this.meals : [];
|
|
31
|
+
}
|
|
32
|
+
componentWillUpdate() {
|
|
33
|
+
// Recomputed before each render rather than in a @Watch, because the property can be
|
|
34
|
+
// replaced before the element upgrades, which @Watch would miss.
|
|
35
|
+
this.normalized = Array.isArray(this.meals) ? this.meals : [];
|
|
36
|
+
}
|
|
37
|
+
onMove = (event, meal) => {
|
|
38
|
+
const select = event.target;
|
|
39
|
+
const to = select.value;
|
|
40
|
+
if (!to || to === this.day)
|
|
41
|
+
return;
|
|
42
|
+
this.rpMoveMeal.emit({ id: meal.id, from: this.day, to });
|
|
43
|
+
// The consumer re-renders from its own data; resetting avoids a stale selection
|
|
44
|
+
// if that render is delayed.
|
|
45
|
+
select.value = '';
|
|
46
|
+
};
|
|
47
|
+
render() {
|
|
48
|
+
const days = Array.isArray(this.days) ? this.days : [];
|
|
49
|
+
const meals = this.normalized;
|
|
50
|
+
return (index.h(index.Host, { key: '689cfc71ca7fc7106e29278d027ce1c36b3830c0' }, index.h("section", { key: 'd15ed9ae6f800be9962c4c1258397ff1043fef76', class: "slot" }, index.h("header", { key: '7f3ce1dcd4e94af5699b378ab1e461a34792acd8', class: "head" }, index.h("h3", { key: 'b62fe42a27f0072370e59a101452f77de8527ae0', class: "day" }, this.dayLabel || this.day), index.h("button", { key: '658191328f041bc2dd7534a3f5614746f26c0f28', type: "button", class: "add", onClick: () => this.rpAddMeal.emit(this.day), "aria-label": `Add a meal to ${this.dayLabel || this.day}` }, "+")), meals.length === 0 ? (index.h("div", { class: "empty" }, index.h("slot", null, index.h("p", { class: "empty-text" }, "No meals planned")))) : (index.h("ul", { class: "meals" }, meals.map((meal) => (index.h("li", { class: "meal", key: meal.id }, index.h("span", { class: "meal-title" }, meal.title), index.h("div", { class: "meal-actions" }, index.h("label", { class: "move" }, index.h("span", { class: "sr-only" }, `Move ${meal.title} to another day`), index.h("select", { class: "move-select", onChange: (event) => this.onMove(event, meal) }, index.h("option", { value: "" }, "Move\u2026"), days
|
|
51
|
+
.filter((day) => day !== this.day)
|
|
52
|
+
.map((day) => (index.h("option", { key: day, value: day }, day))))), index.h("button", { type: "button", class: "remove", onClick: () => this.rpRemoveMeal.emit({ id: meal.id, day: this.day }), "aria-label": `Remove ${meal.title}` }, "\u00D7"))))))))));
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
DaySlot.style = rpDaySlotCss();
|
|
56
|
+
|
|
57
|
+
exports.rp_day_slot = DaySlot;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var index = require('./index-B6uIqnrk.js');
|
|
4
|
+
|
|
5
|
+
const rpFilterChipsCss = () => `.sc-rp-filter-chips-h{display:block;font-family:var(--rp-font-sans)}.chips.sc-rp-filter-chips{display:flex;flex-wrap:wrap;gap:var(--rp-space-2)}.chip.sc-rp-filter-chips{padding:var(--rp-space-1) var(--rp-space-3);font:inherit;font-size:var(--rp-font-size-sm);color:var(--rp-color-text);cursor:pointer;background:var(--rp-color-surface);border:1px solid var(--rp-color-border);border-radius:999px}.chip.sc-rp-filter-chips:hover{background:var(--rp-color-surface-sunken)}.chip.is-selected.sc-rp-filter-chips{color:var(--rp-color-accent-contrast);background:var(--rp-color-accent);border-color:var(--rp-color-accent)}.chip.sc-rp-filter-chips:focus-visible{outline:var(--rp-focus-ring);outline-offset:var(--rp-focus-offset)}`;
|
|
6
|
+
|
|
7
|
+
const FilterChips = class {
|
|
8
|
+
constructor(hostRef) {
|
|
9
|
+
index.registerInstance(this, hostRef);
|
|
10
|
+
this.rpFilterChange = index.createEvent(this, "rpFilterChange");
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Available filter values.
|
|
14
|
+
*
|
|
15
|
+
* An array has no attribute representation, so this must be assigned as a DOM
|
|
16
|
+
* property. Frameworks that set properties on custom elements do this automatically.
|
|
17
|
+
*/
|
|
18
|
+
options = [];
|
|
19
|
+
/** Currently active value, or null when no filter is applied. */
|
|
20
|
+
selected = null;
|
|
21
|
+
/** Accessible label for the group. */
|
|
22
|
+
label = 'Filter';
|
|
23
|
+
/** Fired with the new selection, or null when the filter is cleared. */
|
|
24
|
+
rpFilterChange;
|
|
25
|
+
select(option) {
|
|
26
|
+
this.rpFilterChange.emit(this.selected === option ? null : option);
|
|
27
|
+
}
|
|
28
|
+
render() {
|
|
29
|
+
// A string prop arriving from an HTML attribute would be unusable; tolerate it so
|
|
30
|
+
// the component behaves predictably in a plain .html page.
|
|
31
|
+
const options = Array.isArray(this.options) ? this.options : [];
|
|
32
|
+
return (index.h(index.Host, { key: '656b969af2e55a7c4313bc0c2c422e000811af53' }, index.h("div", { key: '1da1173fc1d6d10a312a43466a0ea799b625caef', class: "chips", role: "group", "aria-label": this.label }, options.map((option) => (index.h("button", { key: option, type: "button", class: { chip: true, 'is-selected': this.selected === option }, "aria-pressed": String(this.selected === option), onClick: () => this.select(option) }, option))))));
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
FilterChips.style = rpFilterChipsCss();
|
|
36
|
+
|
|
37
|
+
const rpIngredientListCss = () => `.sc-rp-ingredient-list-h{display:block;font-family:var(--rp-font-sans);color:var(--rp-color-text)}.heading.sc-rp-ingredient-list{margin:0 0 var(--rp-space-3);font-size:var(--rp-font-size-lg)}.list.sc-rp-ingredient-list{margin:0;padding:0;list-style:none;border:1px solid var(--rp-color-border);border-radius:var(--rp-radius-md);overflow:hidden}.row.sc-rp-ingredient-list{display:flex;justify-content:space-between;gap:var(--rp-space-4);padding:var(--rp-space-2) var(--rp-space-3);font-size:var(--rp-font-size-md)}.row.sc-rp-ingredient-list:nth-child(odd){background:var(--rp-color-surface-sunken)}.measure.sc-rp-ingredient-list{flex-shrink:0;color:var(--rp-color-text-muted);font-variant-numeric:tabular-nums}.empty.sc-rp-ingredient-list{margin:0;font-size:var(--rp-font-size-md);color:var(--rp-color-text-muted)}.note.sc-rp-ingredient-list{margin-top:var(--rp-space-2);font-size:var(--rp-font-size-sm);color:var(--rp-color-text-muted)}.note.sc-rp-ingredient-list:not(:has(*)){display:none}`;
|
|
38
|
+
|
|
39
|
+
const IngredientList = class {
|
|
40
|
+
constructor(hostRef) {
|
|
41
|
+
index.registerInstance(this, hostRef);
|
|
42
|
+
}
|
|
43
|
+
/** Ingredients to display. Must be assigned as a DOM property, not an attribute. */
|
|
44
|
+
items = [];
|
|
45
|
+
render() {
|
|
46
|
+
const items = Array.isArray(this.items) ? this.items : [];
|
|
47
|
+
return (index.h(index.Host, { key: '22708ae6e60de472b2f8e40fb54b33a2ec691ad1' }, index.h("section", { key: '6f922fc37e3c230511bbdf72e55826dfa73e167e', class: "wrap" }, index.h("slot", { key: 'e9f0b16183f5ded825a5c1663caa54dcb6c2d961', name: "heading" }, index.h("h2", { key: 'c68ebf97056bc6309d3c4e79d1796d89eede1325', class: "heading" }, "Ingredients")), items.length === 0 ? (index.h("p", { class: "empty" }, "No ingredients listed for this recipe.")) : (index.h("ul", { class: "list" }, items.map((item, index$1) => (
|
|
48
|
+
// Ingredient names repeat within a recipe (for example, oil listed twice
|
|
49
|
+
// with different measures), so the index is part of the identity.
|
|
50
|
+
index.h("li", { class: "row", key: `${item.name}-${index$1}` }, index.h("span", { class: "name" }, item.name), index.h("span", { class: "measure" }, item.measure)))))), index.h("div", { key: '28d26e1bbb9f14e5dedfb153ff87e44add5cfbfb', class: "note" }, index.h("slot", { key: '20410e710e3c7dbc2ee9f6ad9493984382693687' })))));
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
IngredientList.style = rpIngredientListCss();
|
|
54
|
+
|
|
55
|
+
const rpModalCss = () => `.sc-rp-modal-h{display:contents;font-family:var(--rp-font-sans)}.backdrop.sc-rp-modal{position:fixed;inset:0;z-index:100;display:grid;place-items:center;padding:var(--rp-space-4);background:rgb(0 0 0 / 0.5)}.dialog.sc-rp-modal{display:flex;flex-direction:column;width:min(560px, 100%);max-height:min(80vh, 640px);color:var(--rp-color-text);background:var(--rp-color-surface);border-radius:var(--rp-radius-lg);box-shadow:var(--rp-shadow-lg)}.head.sc-rp-modal{display:flex;gap:var(--rp-space-3);align-items:center;justify-content:space-between;padding:var(--rp-space-4);border-bottom:1px solid var(--rp-color-border)}.heading.sc-rp-modal{margin:0;font-size:var(--rp-font-size-lg)}.close.sc-rp-modal{display:grid;place-items:center;width:32px;height:32px;padding:0;font-size:1.5rem;line-height:1;color:var(--rp-color-text-muted);cursor:pointer;background:none;border:none;border-radius:var(--rp-radius-sm)}.close.sc-rp-modal:hover{background:var(--rp-color-surface-sunken)}.close.sc-rp-modal:focus-visible{outline:var(--rp-focus-ring);outline-offset:var(--rp-focus-offset)}.body.sc-rp-modal{flex:1;padding:var(--rp-space-4);overflow-y:auto}.foot.sc-rp-modal{display:flex;gap:var(--rp-space-2);justify-content:flex-end;padding:var(--rp-space-4);border-top:1px solid var(--rp-color-border)}.foot.sc-rp-modal:not(:has(*)){display:none}@media (prefers-reduced-motion: no-preference){.dialog.sc-rp-modal{animation:rp-modal-in 120ms ease-out}@keyframes rp-modal-in{from{opacity:0;transform:translateY(8px)}}}`;
|
|
56
|
+
|
|
57
|
+
const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
58
|
+
const Modal = class {
|
|
59
|
+
constructor(hostRef) {
|
|
60
|
+
index.registerInstance(this, hostRef);
|
|
61
|
+
this.rpClose = index.createEvent(this, "rpClose");
|
|
62
|
+
}
|
|
63
|
+
get el() { return index.getElement(this); }
|
|
64
|
+
/** Whether the dialog is visible. */
|
|
65
|
+
open = false;
|
|
66
|
+
/** Dialog heading, also used as its accessible name. */
|
|
67
|
+
heading = '';
|
|
68
|
+
/** Fired when the user dismisses the dialog via Escape, the backdrop, or the close button. */
|
|
69
|
+
rpClose;
|
|
70
|
+
previouslyFocused = null;
|
|
71
|
+
onOpenChange(isOpen) {
|
|
72
|
+
if (isOpen) {
|
|
73
|
+
this.previouslyFocused = document.activeElement;
|
|
74
|
+
document.addEventListener('keydown', this.onKeydown);
|
|
75
|
+
// The dialog content renders in the same tick, so defer focus until it exists.
|
|
76
|
+
requestAnimationFrame(() => this.focusFirstField());
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
document.removeEventListener('keydown', this.onKeydown);
|
|
80
|
+
this.previouslyFocused?.focus();
|
|
81
|
+
this.previouslyFocused = null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
componentDidLoad() {
|
|
85
|
+
// A dialog can be mounted already open, in which case @Watch never fires.
|
|
86
|
+
if (this.open)
|
|
87
|
+
this.onOpenChange(true);
|
|
88
|
+
}
|
|
89
|
+
disconnectedCallback() {
|
|
90
|
+
// Without this, every mount of a page containing a modal leaks a document listener.
|
|
91
|
+
document.removeEventListener('keydown', this.onKeydown);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Moves focus to the first focusable control inside the dialog.
|
|
95
|
+
*
|
|
96
|
+
* Exposed as a method because "move focus now" is a one-shot action with no state to
|
|
97
|
+
* represent; a prop would have to be toggled and reset to fire it twice.
|
|
98
|
+
*/
|
|
99
|
+
async focusFirstField() {
|
|
100
|
+
const first = this.el.querySelector(FOCUSABLE);
|
|
101
|
+
first?.focus();
|
|
102
|
+
}
|
|
103
|
+
onKeydown = (event) => {
|
|
104
|
+
if (!this.open)
|
|
105
|
+
return;
|
|
106
|
+
if (event.key === 'Escape') {
|
|
107
|
+
event.preventDefault();
|
|
108
|
+
this.rpClose.emit();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (event.key === 'Tab')
|
|
112
|
+
this.trapFocus(event);
|
|
113
|
+
};
|
|
114
|
+
trapFocus(event) {
|
|
115
|
+
const focusable = Array.from(this.el.querySelectorAll(FOCUSABLE));
|
|
116
|
+
if (focusable.length === 0)
|
|
117
|
+
return;
|
|
118
|
+
const first = focusable[0];
|
|
119
|
+
const last = focusable[focusable.length - 1];
|
|
120
|
+
const active = document.activeElement;
|
|
121
|
+
// Wrap in both directions so Tab never escapes the dialog into the page behind it.
|
|
122
|
+
if (event.shiftKey && active === first) {
|
|
123
|
+
event.preventDefault();
|
|
124
|
+
last.focus();
|
|
125
|
+
}
|
|
126
|
+
else if (!event.shiftKey && active === last) {
|
|
127
|
+
event.preventDefault();
|
|
128
|
+
first.focus();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
onBackdropClick = (event) => {
|
|
132
|
+
if (event.target === event.currentTarget)
|
|
133
|
+
this.rpClose.emit();
|
|
134
|
+
};
|
|
135
|
+
render() {
|
|
136
|
+
if (!this.open)
|
|
137
|
+
return null;
|
|
138
|
+
return (index.h(index.Host, null, index.h("div", { class: "backdrop", onClick: this.onBackdropClick }, index.h("div", { class: "dialog", role: "dialog", "aria-modal": "true", "aria-label": this.heading }, index.h("header", { class: "head" }, index.h("h2", { class: "heading" }, this.heading), index.h("button", { type: "button", class: "close", onClick: () => this.rpClose.emit(), "aria-label": "Close dialog" }, "\u00D7")), index.h("div", { class: "body" }, index.h("slot", null)), index.h("footer", { class: "foot" }, index.h("slot", { name: "footer" }))))));
|
|
139
|
+
}
|
|
140
|
+
static get watchers() { return {
|
|
141
|
+
"open": [{
|
|
142
|
+
"onOpenChange": 0
|
|
143
|
+
}]
|
|
144
|
+
}; }
|
|
145
|
+
};
|
|
146
|
+
Modal.style = rpModalCss();
|
|
147
|
+
|
|
148
|
+
const rpRecipeCardCss = () => `.sc-rp-recipe-card-h{display:block;font-family:var(--rp-font-sans);color:var(--rp-color-text)}.card.sc-rp-recipe-card{display:flex;flex-direction:column;height:100%;overflow:hidden;background:var(--rp-color-surface);border:1px solid var(--rp-color-border);border-radius:var(--rp-radius-lg);box-shadow:var(--rp-shadow-sm)}.media.sc-rp-recipe-card{position:relative;aspect-ratio:4 / 3;background:var(--rp-color-surface-sunken)}.media.sc-rp-recipe-card img.sc-rp-recipe-card{display:block;width:100%;height:100%;object-fit:cover}.media-fallback.sc-rp-recipe-card{width:100%;height:100%;background:linear-gradient(135deg, var(--rp-color-surface-sunken), var(--rp-color-border))}.badges.sc-rp-recipe-card{position:absolute;inset-block-start:var(--rp-space-2);inset-inline-start:var(--rp-space-2);display:flex;flex-wrap:wrap;gap:var(--rp-space-1)}.favorite.sc-rp-recipe-card{position:absolute;inset-block-start:var(--rp-space-2);inset-inline-end:var(--rp-space-2);display:grid;place-items:center;width:36px;height:36px;padding:0;cursor:pointer;background:var(--rp-color-surface);border:1px solid var(--rp-color-border);border-radius:50%;box-shadow:var(--rp-shadow-sm)}.favorite.sc-rp-recipe-card svg.sc-rp-recipe-card{fill:none;stroke:var(--rp-color-text-muted);stroke-width:1.8}[favorite].sc-rp-recipe-card-h .favorite.sc-rp-recipe-card svg.sc-rp-recipe-card{fill:var(--rp-color-favorite);stroke:var(--rp-color-favorite)}.favorite.sc-rp-recipe-card:focus-visible{outline:var(--rp-focus-ring);outline-offset:var(--rp-focus-offset)}.body.sc-rp-recipe-card{flex:1;padding:var(--rp-space-3) var(--rp-space-4)}.title.sc-rp-recipe-card{margin:0;font-size:var(--rp-font-size-lg);line-height:1.3}.category.sc-rp-recipe-card{margin:var(--rp-space-1) 0 0;font-size:var(--rp-font-size-sm);color:var(--rp-color-text-muted)}.actions.sc-rp-recipe-card{display:flex;gap:var(--rp-space-2);align-items:center;padding:0 var(--rp-space-4) var(--rp-space-4)}.actions.sc-rp-recipe-card:not(:has(*)){display:none}`;
|
|
149
|
+
|
|
150
|
+
const RecipeCard = class {
|
|
151
|
+
constructor(hostRef) {
|
|
152
|
+
index.registerInstance(this, hostRef);
|
|
153
|
+
this.rpFavoriteToggle = index.createEvent(this, "rpFavoriteToggle");
|
|
154
|
+
}
|
|
155
|
+
get el() { return index.getElement(this); }
|
|
156
|
+
/** Identifier echoed back in the toggle event so consumers know which card fired. */
|
|
157
|
+
recipeId;
|
|
158
|
+
/**
|
|
159
|
+
* Recipe name, rendered as the card heading.
|
|
160
|
+
*
|
|
161
|
+
* Named `recipeTitle` rather than `title` because every HTMLElement already defines
|
|
162
|
+
* `title` on its prototype; shadowing it produces inconsistent behaviour across browsers.
|
|
163
|
+
*/
|
|
164
|
+
recipeTitle;
|
|
165
|
+
/** Absolute URL of the recipe image. */
|
|
166
|
+
image;
|
|
167
|
+
/** Category label shown under the heading. */
|
|
168
|
+
category;
|
|
169
|
+
/** Whether this recipe is currently a favorite. Reflected for CSS hooks. */
|
|
170
|
+
favorite = false;
|
|
171
|
+
/** Fired when the favorite control is activated. */
|
|
172
|
+
rpFavoriteToggle;
|
|
173
|
+
toggleFavorite = () => {
|
|
174
|
+
// Emits the intended next state. The consumer owns the data and decides whether
|
|
175
|
+
// the change is applied, so this component never writes to its own prop.
|
|
176
|
+
this.rpFavoriteToggle.emit({ recipeId: this.recipeId, favorite: !this.favorite });
|
|
177
|
+
};
|
|
178
|
+
render() {
|
|
179
|
+
return (index.h(index.Host, { key: '7ac4bd65a2fa94815db81c986d17e3dff768334f' }, index.h("article", { key: 'b8ab03b01a7305ebac8fa8c7a512cf670afbd7c9', class: "card" }, index.h("div", { key: 'ddd9892bf42ed7fdd1300d439449ac4c471f11e3', class: "media" }, this.image ? (index.h("img", { src: this.image, alt: "", loading: "lazy", width: "320", height: "240" })) : (index.h("div", { class: "media-fallback", "aria-hidden": "true" })), index.h("div", { key: '211b2f1621e96b2350ff17598094790809c6fe81', class: "badges" }, index.h("slot", { key: 'ab834b8c4ff5c643c35d4f29c7bc9ddab5886e44' })), index.h("button", { key: '67d76da1db3b7d867d64042260cb69b30ea9fa2f', type: "button", class: "favorite", onClick: this.toggleFavorite, "aria-pressed": String(this.favorite), "aria-label": this.favorite ? `Remove ${this.recipeTitle} from favorites` : `Add ${this.recipeTitle} to favorites` }, index.h("svg", { key: 'ad94f74c83aba86af030d380d02ca331bf1edd66', viewBox: "0 0 24 24", width: "20", height: "20", "aria-hidden": "true" }, index.h("path", { key: 'cec837ac8a9e440730ed83f5746fba9833722d43', d: "M12 21s-7.5-4.7-9.3-9A5.2 5.2 0 0 1 12 6.5 5.2 5.2 0 0 1 21.3 12c-1.8 4.3-9.3 9-9.3 9z" })))), index.h("div", { key: '57e619602fb97b1197d1391e1bca37059fad4750', class: "body" }, index.h("h3", { key: '1576688c8d62e883132bb25f7f4f0832e7e102c2', class: "title" }, this.recipeTitle), this.category && index.h("p", { key: 'ca58ac4811bf94d13761ac0fd3fdd63a4b35c057', class: "category" }, this.category)), index.h("div", { key: 'fe7531c32eb5b110fe5aea2c4680a020848633bd', class: "actions" }, index.h("slot", { key: 'daf97a1324f3d2d08d1900d057cab1968be8c9d7', name: "actions" })))));
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
RecipeCard.style = rpRecipeCardCss();
|
|
183
|
+
|
|
184
|
+
const rpSearchBarCss = () => `.sc-rp-search-bar-h{display:block;font-family:var(--rp-font-sans)}.bar.sc-rp-search-bar{display:flex;gap:var(--rp-space-2);align-items:center}.field.sc-rp-search-bar{flex:1;min-width:0;padding:var(--rp-space-2) var(--rp-space-3);font:inherit;font-size:var(--rp-font-size-md);color:var(--rp-color-text);background:var(--rp-color-surface);border:1px solid var(--rp-color-border);border-radius:var(--rp-radius-md)}.field.sc-rp-search-bar::-webkit-search-cancel-button{display:none}.field.sc-rp-search-bar:focus-visible,.submit.sc-rp-search-bar:focus-visible,.clear.sc-rp-search-bar:focus-visible{outline:var(--rp-focus-ring);outline-offset:var(--rp-focus-offset)}.clear.sc-rp-search-bar{display:grid;place-items:center;width:32px;height:32px;padding:0;font-size:1.25rem;line-height:1;color:var(--rp-color-text-muted);cursor:pointer;background:none;border:none;border-radius:50%}.clear.sc-rp-search-bar:hover{background:var(--rp-color-surface-sunken)}.submit.sc-rp-search-bar{padding:var(--rp-space-2) var(--rp-space-4);font:inherit;font-size:var(--rp-font-size-md);font-weight:600;color:var(--rp-color-accent-contrast);cursor:pointer;background:var(--rp-color-accent);border:none;border-radius:var(--rp-radius-md)}`;
|
|
185
|
+
|
|
186
|
+
const SearchBar = class {
|
|
187
|
+
constructor(hostRef) {
|
|
188
|
+
index.registerInstance(this, hostRef);
|
|
189
|
+
this.rpSearch = index.createEvent(this, "rpSearch");
|
|
190
|
+
this.rpClear = index.createEvent(this, "rpClear");
|
|
191
|
+
}
|
|
192
|
+
/** Initial query text. Changing it externally resets the field. */
|
|
193
|
+
value = '';
|
|
194
|
+
/** Placeholder shown when the field is empty. */
|
|
195
|
+
placeholder = 'Search recipes';
|
|
196
|
+
/** Accessible label for the input. */
|
|
197
|
+
label = 'Search recipes';
|
|
198
|
+
/**
|
|
199
|
+
* Live field contents. Held internally so typing does not require a round trip
|
|
200
|
+
* through the consumer, while `value` remains the externally controlled seed.
|
|
201
|
+
*/
|
|
202
|
+
draft = '';
|
|
203
|
+
/** Fired on submit with the trimmed query. */
|
|
204
|
+
rpSearch;
|
|
205
|
+
/** Fired when the user clears a non-empty field. */
|
|
206
|
+
rpClear;
|
|
207
|
+
onValueChange(next) {
|
|
208
|
+
this.draft = next ?? '';
|
|
209
|
+
}
|
|
210
|
+
componentWillLoad() {
|
|
211
|
+
this.draft = this.value ?? '';
|
|
212
|
+
}
|
|
213
|
+
onSubmit = (event) => {
|
|
214
|
+
// The host page owns navigation, so the native GET submission must not fire.
|
|
215
|
+
event.preventDefault();
|
|
216
|
+
this.rpSearch.emit(this.draft.trim());
|
|
217
|
+
};
|
|
218
|
+
onInput = (event) => {
|
|
219
|
+
this.draft = event.target.value;
|
|
220
|
+
};
|
|
221
|
+
onClear = () => {
|
|
222
|
+
this.draft = '';
|
|
223
|
+
this.rpClear.emit();
|
|
224
|
+
};
|
|
225
|
+
render() {
|
|
226
|
+
return (index.h(index.Host, { key: '12550f4e6587473576e211fe1cfa5d075a02399f' }, index.h("form", { key: 'e1a61e15c159e8449770b7df63a649d84b136aa2', class: "bar", role: "search", onSubmit: this.onSubmit }, index.h("input", { key: 'defae81130bf361beae8a57c15633c6595437493', type: "search", class: "field", value: this.draft, placeholder: this.placeholder, "aria-label": this.label, onInput: this.onInput }), this.draft && (index.h("button", { key: '4cd2eac81b32f17d18e89c51a90e0a0d9edf7d01', type: "button", class: "clear", onClick: this.onClear, "aria-label": "Clear search" }, "\u00D7")), index.h("button", { key: 'b07034ed131233623d8edbdd16db3cfd8ffab4b7', type: "submit", class: "submit" }, "Search"))));
|
|
227
|
+
}
|
|
228
|
+
static get watchers() { return {
|
|
229
|
+
"value": [{
|
|
230
|
+
"onValueChange": 0
|
|
231
|
+
}]
|
|
232
|
+
}; }
|
|
233
|
+
};
|
|
234
|
+
SearchBar.style = rpSearchBarCss();
|
|
235
|
+
|
|
236
|
+
exports.rp_filter_chips = FilterChips;
|
|
237
|
+
exports.rp_ingredient_list = IngredientList;
|
|
238
|
+
exports.rp_modal = Modal;
|
|
239
|
+
exports.rp_recipe_card = RecipeCard;
|
|
240
|
+
exports.rp_search_bar = SearchBar;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"entries": [
|
|
3
|
+
"components/rp-day-slot/rp-day-slot.js",
|
|
4
|
+
"components/rp-filter-chips/rp-filter-chips.js",
|
|
5
|
+
"components/rp-ingredient-list/rp-ingredient-list.js",
|
|
6
|
+
"components/rp-modal/rp-modal.js",
|
|
7
|
+
"components/rp-recipe-card/rp-recipe-card.js",
|
|
8
|
+
"components/rp-search-bar/rp-search-bar.js"
|
|
9
|
+
],
|
|
10
|
+
"mixins": [],
|
|
11
|
+
"compiler": {
|
|
12
|
+
"name": "@stencil/core",
|
|
13
|
+
"version": "4.44.1",
|
|
14
|
+
"typescriptVersion": "5.8.3"
|
|
15
|
+
},
|
|
16
|
+
"collections": [],
|
|
17
|
+
"bundles": []
|
|
18
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
:host {
|
|
2
|
+
display: block;
|
|
3
|
+
font-family: var(--rp-font-sans);
|
|
4
|
+
color: var(--rp-color-text);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.slot {
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
height: 100%;
|
|
11
|
+
min-height: 160px;
|
|
12
|
+
background: var(--rp-color-surface);
|
|
13
|
+
border: 1px solid var(--rp-color-border);
|
|
14
|
+
border-radius: var(--rp-radius-md);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.head {
|
|
18
|
+
display: flex;
|
|
19
|
+
gap: var(--rp-space-2);
|
|
20
|
+
align-items: center;
|
|
21
|
+
justify-content: space-between;
|
|
22
|
+
padding: var(--rp-space-2) var(--rp-space-3);
|
|
23
|
+
border-bottom: 1px solid var(--rp-color-border);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.day {
|
|
27
|
+
margin: 0;
|
|
28
|
+
font-size: var(--rp-font-size-md);
|
|
29
|
+
text-transform: capitalize;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.add {
|
|
33
|
+
display: grid;
|
|
34
|
+
place-items: center;
|
|
35
|
+
width: 28px;
|
|
36
|
+
height: 28px;
|
|
37
|
+
padding: 0;
|
|
38
|
+
font-size: 1.125rem;
|
|
39
|
+
line-height: 1;
|
|
40
|
+
color: var(--rp-color-accent-contrast);
|
|
41
|
+
cursor: pointer;
|
|
42
|
+
background: var(--rp-color-accent);
|
|
43
|
+
border: none;
|
|
44
|
+
border-radius: 50%;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.meals {
|
|
48
|
+
flex: 1;
|
|
49
|
+
margin: 0;
|
|
50
|
+
padding: var(--rp-space-2);
|
|
51
|
+
list-style: none;
|
|
52
|
+
display: flex;
|
|
53
|
+
flex-direction: column;
|
|
54
|
+
gap: var(--rp-space-2);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.meal {
|
|
58
|
+
padding: var(--rp-space-2);
|
|
59
|
+
background: var(--rp-color-surface-sunken);
|
|
60
|
+
border-radius: var(--rp-radius-sm);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.meal-title {
|
|
64
|
+
display: block;
|
|
65
|
+
font-size: var(--rp-font-size-sm);
|
|
66
|
+
line-height: 1.35;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.meal-actions {
|
|
70
|
+
display: flex;
|
|
71
|
+
gap: var(--rp-space-1);
|
|
72
|
+
align-items: center;
|
|
73
|
+
justify-content: space-between;
|
|
74
|
+
margin-top: var(--rp-space-2);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.move-select {
|
|
78
|
+
max-width: 100%;
|
|
79
|
+
padding: 2px var(--rp-space-1);
|
|
80
|
+
font: inherit;
|
|
81
|
+
font-size: var(--rp-font-size-sm);
|
|
82
|
+
color: var(--rp-color-text-muted);
|
|
83
|
+
background: var(--rp-color-surface);
|
|
84
|
+
border: 1px solid var(--rp-color-border);
|
|
85
|
+
border-radius: var(--rp-radius-sm);
|
|
86
|
+
text-transform: capitalize;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.remove {
|
|
90
|
+
display: grid;
|
|
91
|
+
place-items: center;
|
|
92
|
+
width: 24px;
|
|
93
|
+
height: 24px;
|
|
94
|
+
padding: 0;
|
|
95
|
+
font-size: 1.125rem;
|
|
96
|
+
line-height: 1;
|
|
97
|
+
color: var(--rp-color-danger);
|
|
98
|
+
cursor: pointer;
|
|
99
|
+
background: none;
|
|
100
|
+
border: none;
|
|
101
|
+
border-radius: var(--rp-radius-sm);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.empty {
|
|
105
|
+
display: grid;
|
|
106
|
+
flex: 1;
|
|
107
|
+
place-items: center;
|
|
108
|
+
padding: var(--rp-space-3);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.empty-text {
|
|
112
|
+
margin: 0;
|
|
113
|
+
font-size: var(--rp-font-size-sm);
|
|
114
|
+
color: var(--rp-color-text-muted);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.add:focus-visible,
|
|
118
|
+
.remove:focus-visible,
|
|
119
|
+
.move-select:focus-visible {
|
|
120
|
+
outline: var(--rp-focus-ring);
|
|
121
|
+
outline-offset: var(--rp-focus-offset);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.sr-only {
|
|
125
|
+
position: absolute;
|
|
126
|
+
width: 1px;
|
|
127
|
+
height: 1px;
|
|
128
|
+
overflow: hidden;
|
|
129
|
+
clip-path: inset(50%);
|
|
130
|
+
white-space: nowrap;
|
|
131
|
+
}
|