@ssejal8/recipe-ui-kit 0.1.0 → 0.1.1

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 (191) hide show
  1. package/README.md +108 -277
  2. package/dist/cjs/index-BEr8BsSA.js +1712 -0
  3. package/dist/cjs/index-BEr8BsSA.js.map +1 -0
  4. package/dist/cjs/index.cjs.js +5 -0
  5. package/dist/cjs/index.cjs.js.map +1 -0
  6. package/dist/cjs/loader.cjs.js +15 -0
  7. package/dist/cjs/loader.cjs.js.map +1 -0
  8. package/dist/cjs/recipe-kit.cjs.js +27 -0
  9. package/dist/cjs/recipe-kit.cjs.js.map +1 -0
  10. package/dist/cjs/rk-badge_10.cjs.entry.js +638 -0
  11. package/dist/cjs/rk-badge_10.cjs.entry.js.map +1 -0
  12. package/dist/collection/collection-manifest.json +22 -0
  13. package/dist/collection/components/rk-badge/rk-badge.js +97 -0
  14. package/dist/collection/components/rk-badge/rk-badge.js.map +1 -0
  15. package/dist/collection/components/rk-chip/rk-chip.js +245 -0
  16. package/dist/collection/components/rk-chip/rk-chip.js.map +1 -0
  17. package/dist/collection/components/rk-empty-state/rk-empty-state.js +138 -0
  18. package/dist/collection/components/rk-empty-state/rk-empty-state.js.map +1 -0
  19. package/dist/collection/components/rk-meal-slot/rk-meal-slot.js +312 -0
  20. package/dist/collection/components/rk-meal-slot/rk-meal-slot.js.map +1 -0
  21. package/dist/collection/components/rk-modal/rk-modal.js +256 -0
  22. package/dist/collection/components/rk-modal/rk-modal.js.map +1 -0
  23. package/dist/collection/components/rk-rating/rk-rating.js +228 -0
  24. package/dist/collection/components/rk-rating/rk-rating.js.map +1 -0
  25. package/dist/collection/components/rk-recipe-card/rk-recipe-card.js +366 -0
  26. package/dist/collection/components/rk-recipe-card/rk-recipe-card.js.map +1 -0
  27. package/dist/collection/components/rk-search-bar/rk-search-bar.js +291 -0
  28. package/dist/collection/components/rk-search-bar/rk-search-bar.js.map +1 -0
  29. package/dist/collection/components/rk-segmented-control/rk-segmented-control.js +196 -0
  30. package/dist/collection/components/rk-segmented-control/rk-segmented-control.js.map +1 -0
  31. package/dist/collection/components/rk-tag-input/rk-tag-input.js +264 -0
  32. package/dist/collection/components/rk-tag-input/rk-tag-input.js.map +1 -0
  33. package/dist/collection/index.js +2 -0
  34. package/dist/collection/index.js.map +1 -0
  35. package/dist/components/index.d.ts +35 -0
  36. package/dist/components/index.js +2 -0
  37. package/dist/components/index.js.map +1 -0
  38. package/dist/components/rk-badge.d.ts +11 -0
  39. package/dist/components/rk-badge.js +2 -0
  40. package/dist/components/rk-badge.js.map +1 -0
  41. package/dist/components/rk-chip.d.ts +11 -0
  42. package/dist/components/rk-chip.js +2 -0
  43. package/dist/components/rk-chip.js.map +1 -0
  44. package/dist/components/rk-empty-state.d.ts +11 -0
  45. package/dist/components/rk-empty-state.js +2 -0
  46. package/dist/components/rk-empty-state.js.map +1 -0
  47. package/dist/components/rk-meal-slot.d.ts +11 -0
  48. package/dist/components/rk-meal-slot.js +2 -0
  49. package/dist/components/rk-meal-slot.js.map +1 -0
  50. package/dist/components/rk-modal.d.ts +11 -0
  51. package/dist/components/rk-modal.js +2 -0
  52. package/dist/components/rk-modal.js.map +1 -0
  53. package/dist/components/rk-rating.d.ts +11 -0
  54. package/dist/components/rk-rating.js +2 -0
  55. package/dist/components/rk-rating.js.map +1 -0
  56. package/dist/components/rk-recipe-card.d.ts +11 -0
  57. package/dist/components/rk-recipe-card.js +2 -0
  58. package/dist/components/rk-recipe-card.js.map +1 -0
  59. package/dist/components/rk-search-bar.d.ts +11 -0
  60. package/dist/components/rk-search-bar.js +2 -0
  61. package/dist/components/rk-search-bar.js.map +1 -0
  62. package/dist/components/rk-segmented-control.d.ts +11 -0
  63. package/dist/components/rk-segmented-control.js +2 -0
  64. package/dist/components/rk-segmented-control.js.map +1 -0
  65. package/dist/components/rk-tag-input.d.ts +11 -0
  66. package/dist/components/rk-tag-input.js +2 -0
  67. package/dist/components/rk-tag-input.js.map +1 -0
  68. package/dist/esm/index-Bxo0C8lB.js +1684 -0
  69. package/dist/esm/index-Bxo0C8lB.js.map +1 -0
  70. package/dist/esm/index.js +4 -0
  71. package/dist/esm/index.js.map +1 -0
  72. package/dist/esm/loader.js +13 -0
  73. package/dist/esm/loader.js.map +1 -0
  74. package/dist/esm/recipe-kit.js +23 -0
  75. package/dist/esm/recipe-kit.js.map +1 -0
  76. package/dist/esm/rk-badge_10.entry.js +627 -0
  77. package/dist/esm/rk-badge_10.entry.js.map +1 -0
  78. package/dist/index.cjs.js +1 -0
  79. package/dist/index.js +1 -0
  80. package/dist/recipe-kit/index.esm.js +2 -0
  81. package/dist/recipe-kit/index.esm.js.map +1 -0
  82. package/dist/recipe-kit/p-Bxo0C8lB.js +3 -0
  83. package/dist/recipe-kit/p-Bxo0C8lB.js.map +1 -0
  84. package/dist/recipe-kit/p-fcb955c7.entry.js +2 -0
  85. package/dist/recipe-kit/p-fcb955c7.entry.js.map +1 -0
  86. package/dist/recipe-kit/recipe-kit.css +1 -0
  87. package/dist/recipe-kit/recipe-kit.esm.js +2 -0
  88. package/dist/recipe-kit/recipe-kit.esm.js.map +1 -0
  89. package/dist/recipe-kit/rk-global.css +104 -0
  90. package/dist/types/components/rk-badge/rk-badge.d.ts +15 -0
  91. package/dist/types/components/rk-chip/rk-chip.d.ts +37 -0
  92. package/dist/types/components/rk-empty-state/rk-empty-state.d.ts +19 -0
  93. package/dist/types/components/rk-meal-slot/rk-meal-slot.d.ts +53 -0
  94. package/dist/types/components/rk-modal/rk-modal.d.ts +41 -0
  95. package/dist/types/components/rk-rating/rk-rating.d.ts +26 -0
  96. package/dist/types/components/rk-recipe-card/rk-recipe-card.d.ts +67 -0
  97. package/dist/types/components/rk-search-bar/rk-search-bar.d.ts +39 -0
  98. package/dist/types/components/rk-segmented-control/rk-segmented-control.d.ts +37 -0
  99. package/dist/types/components/rk-tag-input/rk-tag-input.d.ts +35 -0
  100. package/{packages/recipe-kit-stencil/src → dist/types}/components.d.ts +1 -1
  101. package/{packages/recipe-kit-stencil/src/index.ts → dist/types/index.d.ts} +0 -2
  102. package/dist/types/stencil-public-runtime.d.ts +1873 -0
  103. package/loader/cdn.js +1 -0
  104. package/loader/index.cjs.js +1 -0
  105. package/loader/index.d.ts +24 -0
  106. package/loader/index.es2017.js +1 -0
  107. package/loader/index.js +2 -0
  108. package/package.json +84 -22
  109. package/.claude/settings.local.json +0 -10
  110. package/.npmr +0 -1
  111. package/apps/recipe-web/package.json +0 -31
  112. package/apps/recipe-web/src/app.css +0 -418
  113. package/apps/recipe-web/src/app.d.ts +0 -32
  114. package/apps/recipe-web/src/app.html +0 -25
  115. package/apps/recipe-web/src/lib/api/mealdb.ts +0 -240
  116. package/apps/recipe-web/src/lib/components/AppHeader.svelte +0 -290
  117. package/apps/recipe-web/src/lib/components/ConfirmDialog.svelte +0 -46
  118. package/apps/recipe-web/src/lib/components/Modal.svelte +0 -33
  119. package/apps/recipe-web/src/lib/components/RecipeCard.svelte +0 -100
  120. package/apps/recipe-web/src/lib/components/RecipeForm.svelte +0 -560
  121. package/apps/recipe-web/src/lib/components/RecipeGrid.svelte +0 -77
  122. package/apps/recipe-web/src/lib/components/RecipePickerDialog.svelte +0 -185
  123. package/apps/recipe-web/src/lib/components/Toaster.svelte +0 -111
  124. package/apps/recipe-web/src/lib/domain/recipe.ts +0 -130
  125. package/apps/recipe-web/src/lib/domain/validation.ts +0 -189
  126. package/apps/recipe-web/src/lib/stencil/WebComponent.svelte +0 -145
  127. package/apps/recipe-web/src/lib/stencil/register.ts +0 -28
  128. package/apps/recipe-web/src/lib/stores/favorites.svelte.ts +0 -81
  129. package/apps/recipe-web/src/lib/stores/myRecipes.svelte.ts +0 -98
  130. package/apps/recipe-web/src/lib/stores/persisted.svelte.ts +0 -73
  131. package/apps/recipe-web/src/lib/stores/planner.svelte.ts +0 -275
  132. package/apps/recipe-web/src/lib/stores/theme.svelte.ts +0 -67
  133. package/apps/recipe-web/src/lib/stores/toast.svelte.ts +0 -66
  134. package/apps/recipe-web/src/routes/+error.svelte +0 -36
  135. package/apps/recipe-web/src/routes/+layout.svelte +0 -62
  136. package/apps/recipe-web/src/routes/+page.svelte +0 -352
  137. package/apps/recipe-web/src/routes/+page.ts +0 -107
  138. package/apps/recipe-web/src/routes/favorites/+page.svelte +0 -170
  139. package/apps/recipe-web/src/routes/my-recipes/+layout.ts +0 -55
  140. package/apps/recipe-web/src/routes/my-recipes/+page.svelte +0 -180
  141. package/apps/recipe-web/src/routes/my-recipes/[id]/edit/+page.svelte +0 -104
  142. package/apps/recipe-web/src/routes/my-recipes/new/+page.svelte +0 -67
  143. package/apps/recipe-web/src/routes/planner/+page.svelte +0 -422
  144. package/apps/recipe-web/src/routes/recipes/[id]/+page.svelte +0 -538
  145. package/apps/recipe-web/src/routes/recipes/[id]/+page.ts +0 -24
  146. package/apps/recipe-web/static/favicon.svg +0 -8
  147. package/apps/recipe-web/static/robots.txt +0 -2
  148. package/apps/recipe-web/svelte.config.js +0 -35
  149. package/apps/recipe-web/tsconfig.json +0 -14
  150. package/apps/recipe-web/vite.config.ts +0 -23
  151. package/packages/recipe-kit-stencil/README.md +0 -185
  152. package/packages/recipe-kit-stencil/package-lock.json +0 -4772
  153. package/packages/recipe-kit-stencil/package.json +0 -91
  154. package/packages/recipe-kit-stencil/src/components/rk-badge/readme.md +0 -38
  155. package/packages/recipe-kit-stencil/src/components/rk-badge/rk-badge.tsx +0 -31
  156. package/packages/recipe-kit-stencil/src/components/rk-chip/readme.md +0 -52
  157. package/packages/recipe-kit-stencil/src/components/rk-chip/rk-chip.tsx +0 -105
  158. package/packages/recipe-kit-stencil/src/components/rk-empty-state/readme.md +0 -44
  159. package/packages/recipe-kit-stencil/src/components/rk-empty-state/rk-empty-state.tsx +0 -50
  160. package/packages/recipe-kit-stencil/src/components/rk-meal-slot/readme.md +0 -58
  161. package/packages/recipe-kit-stencil/src/components/rk-meal-slot/rk-meal-slot.tsx +0 -182
  162. package/packages/recipe-kit-stencil/src/components/rk-modal/readme.md +0 -57
  163. package/packages/recipe-kit-stencil/src/components/rk-modal/rk-modal.tsx +0 -176
  164. package/packages/recipe-kit-stencil/src/components/rk-rating/readme.md +0 -42
  165. package/packages/recipe-kit-stencil/src/components/rk-rating/rk-rating.tsx +0 -114
  166. package/packages/recipe-kit-stencil/src/components/rk-recipe-card/readme.md +0 -66
  167. package/packages/recipe-kit-stencil/src/components/rk-recipe-card/rk-recipe-card.tsx +0 -179
  168. package/packages/recipe-kit-stencil/src/components/rk-recipe-card/test/rk-recipe-card.spec.tsx +0 -70
  169. package/packages/recipe-kit-stencil/src/components/rk-search-bar/readme.md +0 -67
  170. package/packages/recipe-kit-stencil/src/components/rk-search-bar/rk-search-bar.tsx +0 -152
  171. package/packages/recipe-kit-stencil/src/components/rk-search-bar/test/rk-search-bar.spec.tsx +0 -93
  172. package/packages/recipe-kit-stencil/src/components/rk-segmented-control/readme.md +0 -41
  173. package/packages/recipe-kit-stencil/src/components/rk-segmented-control/rk-segmented-control.tsx +0 -115
  174. package/packages/recipe-kit-stencil/src/components/rk-tag-input/readme.md +0 -47
  175. package/packages/recipe-kit-stencil/src/components/rk-tag-input/rk-tag-input.tsx +0 -181
  176. package/packages/recipe-kit-stencil/src/components/rk-tag-input/test/rk-tag-input.spec.tsx +0 -76
  177. package/packages/recipe-kit-stencil/src/index.html +0 -259
  178. package/packages/recipe-kit-stencil/stencil.config.ts +0 -41
  179. package/packages/recipe-kit-stencil/tsconfig.json +0 -24
  180. /package/{packages/recipe-kit-stencil/LICENSE → LICENSE} +0 -0
  181. /package/{packages/recipe-kit-stencil/src → dist/collection}/components/rk-badge/rk-badge.css +0 -0
  182. /package/{packages/recipe-kit-stencil/src → dist/collection}/components/rk-chip/rk-chip.css +0 -0
  183. /package/{packages/recipe-kit-stencil/src → dist/collection}/components/rk-empty-state/rk-empty-state.css +0 -0
  184. /package/{packages/recipe-kit-stencil/src → dist/collection}/components/rk-meal-slot/rk-meal-slot.css +0 -0
  185. /package/{packages/recipe-kit-stencil/src → dist/collection}/components/rk-modal/rk-modal.css +0 -0
  186. /package/{packages/recipe-kit-stencil/src → dist/collection}/components/rk-rating/rk-rating.css +0 -0
  187. /package/{packages/recipe-kit-stencil/src → dist/collection}/components/rk-recipe-card/rk-recipe-card.css +0 -0
  188. /package/{packages/recipe-kit-stencil/src → dist/collection}/components/rk-search-bar/rk-search-bar.css +0 -0
  189. /package/{packages/recipe-kit-stencil/src → dist/collection}/components/rk-segmented-control/rk-segmented-control.css +0 -0
  190. /package/{packages/recipe-kit-stencil/src → dist/collection}/components/rk-tag-input/rk-tag-input.css +0 -0
  191. /package/{packages/recipe-kit-stencil/src/global → dist/collection}/rk-global.css +0 -0
@@ -1,57 +0,0 @@
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/)*
@@ -1,176 +0,0 @@
1
- import { Component, Element, Event, EventEmitter, h, Host, Prop, State, Watch } from '@stencil/core';
2
-
3
- /**
4
- * An accessible dialog with a backdrop, Escape handling and focus restoration.
5
- *
6
- * The component is presentational: it emits `rkModalClose` and lets the host
7
- * application own the `open` state.
8
- *
9
- * @slot - Dialog body.
10
- * @slot header - Replaces the default heading row entirely.
11
- * @slot footer - Action row pinned to the bottom of the dialog.
12
- */
13
- @Component({
14
- tag: 'rk-modal',
15
- styleUrl: 'rk-modal.css',
16
- shadow: true,
17
- })
18
- export class RkModal {
19
- @Element() host!: HTMLElement;
20
-
21
- /** Controls visibility. */
22
- @Prop() open = false;
23
-
24
- /** Heading text for the default header. */
25
- @Prop() heading = '';
26
-
27
- /** Optional supporting line under the heading. */
28
- @Prop() subheading?: string;
29
-
30
- /** Dialog width preset. */
31
- @Prop() size: 'sm' | 'md' | 'lg' = 'md';
32
-
33
- /** Set to false to require an explicit action to dismiss. */
34
- @Prop() dismissible = true;
35
-
36
- /** Emitted when the user requests dismissal (X, backdrop or Escape). */
37
- @Event({ eventName: 'rkModalClose' }) rkModalClose!: EventEmitter<void>;
38
-
39
- @State() private hasFooter = false;
40
-
41
- private previouslyFocused?: HTMLElement;
42
- private panelEl?: HTMLElement;
43
-
44
- /** Collapses the footer region when no action row was projected into it. */
45
- private syncFooterSlot = () => {
46
- this.hasFooter = Array.from(this.host.children).some(node => node.getAttribute('slot') === 'footer');
47
- };
48
-
49
- @Watch('open')
50
- protected onOpenChange(isOpen: boolean, wasOpen: boolean) {
51
- if (isOpen === wasOpen) return;
52
-
53
- if (isOpen) {
54
- this.previouslyFocused = document.activeElement as HTMLElement;
55
- document.addEventListener('keydown', this.handleGlobalKeyDown);
56
- this.lockScroll(true);
57
- // Wait a frame so the panel exists before moving focus into it.
58
- requestAnimationFrame(() => this.focusFirst());
59
- } else {
60
- document.removeEventListener('keydown', this.handleGlobalKeyDown);
61
- this.lockScroll(false);
62
- this.previouslyFocused?.focus?.();
63
- this.previouslyFocused = undefined;
64
- }
65
- }
66
-
67
- componentWillLoad() {
68
- this.syncFooterSlot();
69
- }
70
-
71
- componentDidLoad() {
72
- if (this.open) this.onOpenChange(true, false);
73
- }
74
-
75
- disconnectedCallback() {
76
- document.removeEventListener('keydown', this.handleGlobalKeyDown);
77
- this.lockScroll(false);
78
- }
79
-
80
- private lockScroll(locked: boolean) {
81
- if (typeof document === 'undefined') return;
82
- document.documentElement.style.overflow = locked ? 'hidden' : '';
83
- }
84
-
85
- private focusableElements(): HTMLElement[] {
86
- if (!this.panelEl) return [];
87
- const selector = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
88
- // Slotted content lives in the light DOM, so both trees have to be queried.
89
- const inShadow = Array.from(this.panelEl.querySelectorAll<HTMLElement>(selector));
90
- const inLight = Array.from(this.host.querySelectorAll<HTMLElement>(selector));
91
- return [...inShadow, ...inLight].filter(el => el.offsetParent !== null || el === document.activeElement);
92
- }
93
-
94
- private focusFirst() {
95
- const [first] = this.focusableElements();
96
- (first ?? this.panelEl)?.focus?.();
97
- }
98
-
99
- private handleGlobalKeyDown = (event: KeyboardEvent) => {
100
- if (!this.open) return;
101
-
102
- if (event.key === 'Escape' && this.dismissible) {
103
- event.preventDefault();
104
- this.rkModalClose.emit();
105
- return;
106
- }
107
-
108
- if (event.key === 'Tab') {
109
- const focusable = this.focusableElements();
110
- if (focusable.length === 0) return;
111
- const first = focusable[0];
112
- const last = focusable[focusable.length - 1];
113
- const active = (this.host.shadowRoot?.activeElement ?? document.activeElement) as HTMLElement;
114
-
115
- if (event.shiftKey && active === first) {
116
- event.preventDefault();
117
- last.focus();
118
- } else if (!event.shiftKey && active === last) {
119
- event.preventDefault();
120
- first.focus();
121
- }
122
- }
123
- };
124
-
125
- private handleBackdrop = (event: MouseEvent) => {
126
- if (!this.dismissible) return;
127
- if (event.target === event.currentTarget) this.rkModalClose.emit();
128
- };
129
-
130
- render() {
131
- if (!this.open) return <Host aria-hidden="true" />;
132
-
133
- return (
134
- <Host>
135
- <div class="backdrop" part="backdrop" onClick={this.handleBackdrop}>
136
- <div
137
- class={{ panel: true, [`size-${this.size}`]: true }}
138
- part="panel"
139
- role="dialog"
140
- aria-modal="true"
141
- aria-label={this.heading || 'Dialog'}
142
- tabindex={-1}
143
- ref={el => (this.panelEl = el)}
144
- >
145
- <header class="header" part="header">
146
- <slot name="header">
147
- <div class="titles">
148
- <h2 class="heading" part="heading">
149
- {this.heading}
150
- </h2>
151
- {this.subheading && <p class="subheading">{this.subheading}</p>}
152
- </div>
153
- </slot>
154
-
155
- {this.dismissible && (
156
- <button class="close" part="close" type="button" aria-label="Close dialog" onClick={() => this.rkModalClose.emit()}>
157
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
158
- <path d="M6 6l12 12M18 6 6 18" />
159
- </svg>
160
- </button>
161
- )}
162
- </header>
163
-
164
- <div class="body" part="body">
165
- <slot />
166
- </div>
167
-
168
- <footer class={{ footer: true, 'is-empty': !this.hasFooter }} part="footer">
169
- <slot name="footer" onSlotchange={this.syncFooterSlot} />
170
- </footer>
171
- </div>
172
- </div>
173
- </Host>
174
- );
175
- }
176
- }
@@ -1,42 +0,0 @@
1
- # rk-rating
2
-
3
-
4
-
5
- <!-- Auto Generated Below -->
6
-
7
-
8
- ## Overview
9
-
10
- A star rating that works as a read-only display or an interactive input.
11
-
12
- ## Properties
13
-
14
- | Property | Attribute | Description | Type | Default |
15
- | ----------- | ------------ | ------------------------------------------------------- | --------- | ----------- |
16
- | `count` | `count` | Optional count of ratings, rendered as "(24)". | `number` | `undefined` |
17
- | `label` | `label` | Accessible label for the group. | `string` | `'Rating'` |
18
- | `max` | `max` | Number of stars. | `number` | `5` |
19
- | `readonly` | `readonly` | When true the stars are display-only and not focusable. | `boolean` | `false` |
20
- | `showValue` | `show-value` | Renders the numeric value next to the stars. | `boolean` | `false` |
21
- | `size` | `size` | Star size in pixels. | `number` | `18` |
22
- | `value` | `value` | Current rating. Supports halves when `readonly`. | `number` | `0` |
23
-
24
-
25
- ## Events
26
-
27
- | Event | Description | Type |
28
- | ---------------- | ----------------------------------------- | --------------------- |
29
- | `rkRatingChange` | Emitted when the user picks a new rating. | `CustomEvent<number>` |
30
-
31
-
32
- ## Shadow Parts
33
-
34
- | Part | Description |
35
- | --------- | ----------- |
36
- | `"stars"` | |
37
- | `"value"` | |
38
-
39
-
40
- ----------------------------------------------
41
-
42
- *Built with [StencilJS](https://stenciljs.com/)*
@@ -1,114 +0,0 @@
1
- import { Component, Event, EventEmitter, h, Host, Prop, State } from '@stencil/core';
2
-
3
- /**
4
- * A star rating that works as a read-only display or an interactive input.
5
- */
6
- @Component({
7
- tag: 'rk-rating',
8
- styleUrl: 'rk-rating.css',
9
- shadow: true,
10
- })
11
- export class RkRating {
12
- /** Current rating. Supports halves when `readonly`. */
13
- @Prop({ mutable: true }) value = 0;
14
-
15
- /** Number of stars. */
16
- @Prop() max = 5;
17
-
18
- /** When true the stars are display-only and not focusable. */
19
- @Prop() readonly = false;
20
-
21
- /** Star size in pixels. */
22
- @Prop() size = 18;
23
-
24
- /** Accessible label for the group. */
25
- @Prop() label = 'Rating';
26
-
27
- /** Renders the numeric value next to the stars. */
28
- @Prop() showValue = false;
29
-
30
- /** Optional count of ratings, rendered as "(24)". */
31
- @Prop() count?: number;
32
-
33
- /** Emitted when the user picks a new rating. */
34
- @Event({ eventName: 'rkRatingChange' }) rkRatingChange!: EventEmitter<number>;
35
-
36
- @State() private hovered = 0;
37
-
38
- private select(next: number) {
39
- if (this.readonly) return;
40
- // Clicking the active star clears the rating, which is the usual affordance.
41
- const resolved = next === this.value ? 0 : next;
42
- this.value = resolved;
43
- this.rkRatingChange.emit(resolved);
44
- }
45
-
46
- private handleKeyDown = (event: KeyboardEvent) => {
47
- if (this.readonly) return;
48
- const step = event.key === 'ArrowRight' || event.key === 'ArrowUp' ? 1 : event.key === 'ArrowLeft' || event.key === 'ArrowDown' ? -1 : 0;
49
- if (step === 0) return;
50
- event.preventDefault();
51
- const next = Math.min(this.max, Math.max(0, Math.round(this.value) + step));
52
- this.value = next;
53
- this.rkRatingChange.emit(next);
54
- };
55
-
56
- render() {
57
- const active = this.hovered || this.value;
58
- const stars = Array.from({ length: Math.max(1, Math.round(this.max)) }, (_, index) => index + 1);
59
-
60
- return (
61
- <Host style={{ '--rk-star-size': `${this.size}px` }}>
62
- <div
63
- class={{ stars: true, 'is-readonly': this.readonly }}
64
- part="stars"
65
- role={this.readonly ? 'img' : 'slider'}
66
- aria-label={`${this.label}: ${this.value} out of ${this.max}`}
67
- aria-valuenow={this.readonly ? null : String(this.value)}
68
- aria-valuemin={this.readonly ? null : '0'}
69
- aria-valuemax={this.readonly ? null : String(this.max)}
70
- tabindex={this.readonly ? null : 0}
71
- onKeyDown={this.handleKeyDown}
72
- onMouseLeave={() => (this.hovered = 0)}
73
- >
74
- {stars.map(star => {
75
- // Fractional fill only matters in read-only display mode.
76
- const fill = this.readonly ? Math.min(1, Math.max(0, this.value - (star - 1))) : star <= active ? 1 : 0;
77
-
78
- return (
79
- <span
80
- class="star-wrap"
81
- key={star}
82
- onMouseEnter={() => !this.readonly && (this.hovered = star)}
83
- onClick={() => this.select(star)}
84
- aria-hidden="true"
85
- >
86
- <svg class="star" viewBox="0 0 24 24">
87
- <path
88
- class="star-bg"
89
- d="M12 2.6l2.86 5.98 6.54.87-4.8 4.5 1.2 6.45L12 17.3l-5.8 3.1 1.2-6.45-4.8-4.5 6.54-.87z"
90
- />
91
- <clipPath id={`clip-${star}`}>
92
- <rect x="0" y="0" width={24 * fill} height="24" />
93
- </clipPath>
94
- <path
95
- class="star-fg"
96
- clip-path={`url(#clip-${star})`}
97
- d="M12 2.6l2.86 5.98 6.54.87-4.8 4.5 1.2 6.45L12 17.3l-5.8 3.1 1.2-6.45-4.8-4.5 6.54-.87z"
98
- />
99
- </svg>
100
- </span>
101
- );
102
- })}
103
-
104
- {this.showValue && (
105
- <span class="value" part="value">
106
- {this.value ? this.value.toFixed(1) : '—'}
107
- {typeof this.count === 'number' && <span class="count">({this.count})</span>}
108
- </span>
109
- )}
110
- </div>
111
- </Host>
112
- );
113
- }
114
- }
@@ -1,66 +0,0 @@
1
- # rk-recipe-card
2
-
3
-
4
-
5
- <!-- Auto Generated Below -->
6
-
7
-
8
- ## Overview
9
-
10
- The primary recipe tile used across discovery, favorites and search results.
11
-
12
- All data arrives as individual primitive properties rather than one object
13
- prop, so the component works with plain HTML attributes in any framework.
14
-
15
- ## Properties
16
-
17
- | Property | Attribute | Description | Type | Default |
18
- | ----------------------- | --------------- | ------------------------------------------------------------------------- | ------------------- | ----------- |
19
- | `area` | `area` | Cuisine / region, e.g. "Italian". | `string` | `undefined` |
20
- | `category` | `category` | Category, e.g. "Dessert". | `string` | `undefined` |
21
- | `compact` | `compact` | Renders a compact horizontal layout, for dense lists and the planner. | `boolean` | `false` |
22
- | `favorite` | `favorite` | Whether this recipe is currently favorited. | `boolean` | `false` |
23
- | `hideFavorite` | `hide-favorite` | Hides the favorite button entirely (e.g. inside the planner). | `boolean` | `false` |
24
- | `href` | `href` | Real href used for accessibility, SEO and middle-click / open-in-new-tab. | `string` | `undefined` |
25
- | `image` | `image` | Thumbnail URL. | `string` | `undefined` |
26
- | `minutes` | `minutes` | Total time in minutes. Hidden when omitted. | `number` | `undefined` |
27
- | `name` | `name` | Recipe title. | `string` | `''` |
28
- | `origin` | `origin` | `custom` recipes are user-authored and get an "Owned" badge. | `"api" \| "custom"` | `'api'` |
29
- | `recipeId` _(required)_ | `recipe-id` | Stable identifier echoed back in every event payload. | `string` | `undefined` |
30
- | `servings` | `servings` | Number of servings. Hidden when omitted. | `number` | `undefined` |
31
-
32
-
33
- ## Events
34
-
35
- | Event | Description | Type |
36
- | ------------------ | -------------------------------------------------------------------- | --------------------------------------- |
37
- | `rkCardSelect` | Emitted on primary activation. The host app performs the navigation. | `CustomEvent<RkRecipeCardSelectDetail>` |
38
- | `rkFavoriteToggle` | Emitted when the favorite button is pressed. | `CustomEvent<RkFavoriteToggleDetail>` |
39
-
40
-
41
- ## Slots
42
-
43
- | Slot | Description |
44
- | ----------- | ---------------------------------------------------------------- |
45
- | | Extra metadata rendered under the title (badges, tags, ratings). |
46
- | `"actions"` | Buttons rendered in the card footer (e.g. "Add to plan"). |
47
-
48
-
49
- ## Shadow Parts
50
-
51
- | Part | Description |
52
- | ------------------- | ----------- |
53
- | `"body"` | |
54
- | `"card"` | |
55
- | `"extra"` | |
56
- | `"favorite-button"` | |
57
- | `"footer"` | |
58
- | `"media"` | |
59
- | `"meta"` | |
60
- | `"owned-badge"` | |
61
- | `"title"` | |
62
-
63
-
64
- ----------------------------------------------
65
-
66
- *Built with [StencilJS](https://stenciljs.com/)*
@@ -1,179 +0,0 @@
1
- import { Component, Element, Event, EventEmitter, h, Prop, State } from '@stencil/core';
2
-
3
- export interface RkRecipeCardSelectDetail {
4
- recipeId: string;
5
- name: string;
6
- origin: 'api' | 'custom';
7
- }
8
-
9
- export interface RkFavoriteToggleDetail {
10
- recipeId: string;
11
- name: string;
12
- origin: 'api' | 'custom';
13
- /** The state the host app should move *to*. */
14
- favorite: boolean;
15
- }
16
-
17
- /**
18
- * The primary recipe tile used across discovery, favorites and search results.
19
- *
20
- * All data arrives as individual primitive properties rather than one object
21
- * prop, so the component works with plain HTML attributes in any framework.
22
- *
23
- * @slot - Extra metadata rendered under the title (badges, tags, ratings).
24
- * @slot actions - Buttons rendered in the card footer (e.g. "Add to plan").
25
- */
26
- @Component({
27
- tag: 'rk-recipe-card',
28
- styleUrl: 'rk-recipe-card.css',
29
- shadow: true,
30
- })
31
- export class RkRecipeCard {
32
- @Element() host!: HTMLElement;
33
-
34
- /** Stable identifier echoed back in every event payload. */
35
- @Prop() recipeId!: string;
36
-
37
- /** Recipe title. */
38
- @Prop() name = '';
39
-
40
- /** Thumbnail URL. */
41
- @Prop() image?: string;
42
-
43
- /** Category, e.g. "Dessert". */
44
- @Prop() category?: string;
45
-
46
- /** Cuisine / region, e.g. "Italian". */
47
- @Prop() area?: string;
48
-
49
- /** Total time in minutes. Hidden when omitted. */
50
- @Prop() minutes?: number;
51
-
52
- /** Number of servings. Hidden when omitted. */
53
- @Prop() servings?: number;
54
-
55
- /** Whether this recipe is currently favorited. */
56
- @Prop() favorite = false;
57
-
58
- /** `custom` recipes are user-authored and get an "Owned" badge. */
59
- @Prop() origin: 'api' | 'custom' = 'api';
60
-
61
- /** Real href used for accessibility, SEO and middle-click / open-in-new-tab. */
62
- @Prop() href?: string;
63
-
64
- /** Renders a compact horizontal layout, for dense lists and the planner. */
65
- @Prop() compact = false;
66
-
67
- /** Hides the favorite button entirely (e.g. inside the planner). */
68
- @Prop() hideFavorite = false;
69
-
70
- /** Emitted on primary activation. The host app performs the navigation. */
71
- @Event({ eventName: 'rkCardSelect' }) rkCardSelect!: EventEmitter<RkRecipeCardSelectDetail>;
72
-
73
- /** Emitted when the favorite button is pressed. */
74
- @Event({ eventName: 'rkFavoriteToggle' }) rkFavoriteToggle!: EventEmitter<RkFavoriteToggleDetail>;
75
-
76
- @State() private imageFailed = false;
77
- @State() private hasExtra = false;
78
- @State() private hasActions = false;
79
-
80
- componentWillLoad() {
81
- this.syncSlots();
82
- }
83
-
84
- /**
85
- * Slot content is optional, and an empty wrapper would still paint padding and
86
- * a border. Tracking what was actually projected lets the card collapse those
87
- * regions instead. Re-run on `slotchange` so framework-driven updates count.
88
- */
89
- private syncSlots = () => {
90
- const elements = Array.from(this.host.children);
91
- this.hasActions = elements.some(node => node.getAttribute('slot') === 'actions');
92
- this.hasExtra =
93
- elements.some(node => !node.hasAttribute('slot')) ||
94
- Array.from(this.host.childNodes).some(node => node.nodeType === Node.TEXT_NODE && (node.textContent ?? '').trim() !== '');
95
- };
96
-
97
- private get detail(): RkRecipeCardSelectDetail {
98
- return { recipeId: this.recipeId, name: this.name, origin: this.origin };
99
- }
100
-
101
- private handleSelect = (event: MouseEvent) => {
102
- // Let the browser handle modified clicks so "open in new tab" keeps working.
103
- if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) {
104
- return;
105
- }
106
- event.preventDefault();
107
- this.rkCardSelect.emit(this.detail);
108
- };
109
-
110
- private handleFavorite = (event: MouseEvent) => {
111
- event.preventDefault();
112
- event.stopPropagation();
113
- this.rkFavoriteToggle.emit({ ...this.detail, favorite: !this.favorite });
114
- };
115
-
116
- render() {
117
- const showImage = this.image && !this.imageFailed;
118
- const href = this.href ?? `#${this.recipeId}`;
119
-
120
- return (
121
- <article class={{ card: true, 'is-compact': this.compact }} part="card">
122
- <a class="media" part="media" href={href} tabindex={-1} aria-hidden="true" onClick={this.handleSelect}>
123
- {showImage ? (
124
- <img src={this.image} alt="" loading="lazy" decoding="async" onError={() => (this.imageFailed = true)} />
125
- ) : (
126
- <span class="media-fallback" aria-hidden="true">
127
- 🍲
128
- </span>
129
- )}
130
-
131
- {this.origin === 'custom' && (
132
- <span class="owned" part="owned-badge">
133
- Yours
134
- </span>
135
- )}
136
- </a>
137
-
138
- {!this.hideFavorite && (
139
- <button
140
- class={{ fav: true, 'is-on': this.favorite }}
141
- part="favorite-button"
142
- type="button"
143
- onClick={this.handleFavorite}
144
- aria-pressed={String(this.favorite)}
145
- aria-label={this.favorite ? `Remove ${this.name} from favorites` : `Add ${this.name} to favorites`}
146
- title={this.favorite ? 'Remove from favorites' : 'Add to favorites'}
147
- >
148
- <svg viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round" aria-hidden="true">
149
- <path d="M12 20.5 4.6 13.3a4.7 4.7 0 0 1 0-6.7 4.7 4.7 0 0 1 6.7 0l.7.7.7-.7a4.7 4.7 0 0 1 6.7 0 4.7 4.7 0 0 1 0 6.7Z" />
150
- </svg>
151
- </button>
152
- )}
153
-
154
- <div class="body" part="body">
155
- <h3 class="title" part="title">
156
- <a href={href} onClick={this.handleSelect}>
157
- {this.name}
158
- </a>
159
- </h3>
160
-
161
- <p class="meta" part="meta">
162
- {this.category && <span class="meta-item">{this.category}</span>}
163
- {this.area && <span class="meta-item">{this.area}</span>}
164
- {this.minutes > 0 && <span class="meta-item">{this.minutes} min</span>}
165
- {this.servings > 0 && <span class="meta-item">Serves {this.servings}</span>}
166
- </p>
167
-
168
- <div class={{ extra: true, 'is-empty': !this.hasExtra }} part="extra">
169
- <slot onSlotchange={this.syncSlots} />
170
- </div>
171
- </div>
172
-
173
- <footer class={{ footer: true, 'is-empty': !this.hasActions }} part="footer">
174
- <slot name="actions" onSlotchange={this.syncSlots} />
175
- </footer>
176
- </article>
177
- );
178
- }
179
- }