@happyvertical/smrt-products 0.34.6 → 0.34.8

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.
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { Form, Input, Textarea } from '@happyvertical/smrt-ui/forms';
2
3
  import { useI18n } from '@happyvertical/smrt-ui/i18n';
3
4
  import { Button } from '@happyvertical/smrt-ui/ui';
4
5
  import { M } from '../i18n.js';
@@ -66,128 +67,126 @@ function handleSubmit(event: Event) {
66
67
  }
67
68
  </script>
68
69
 
69
- <form onsubmit={handleSubmit} class="product-form">
70
- <div class="form-group">
71
- <label for="name">{t(M['products.product_form.name_label'])}</label>
72
- <input
73
- id="name"
74
- type="text"
75
- bind:value={formData.name}
76
- disabled={loading}
77
- class="form-input"
78
- class:error={errors.name}
79
- placeholder={t(M['products.product_form.name_placeholder'])}
80
- />
81
- {#if errors.name}
82
- <span class="error-message">{errors.name}</span>
83
- {/if}
84
- </div>
85
-
86
- <div class="form-group">
87
- <label for="description">Description</label>
88
- <textarea
89
- id="description"
90
- bind:value={formData.description}
91
- disabled={loading}
92
- class="form-textarea"
93
- placeholder={t(M['products.product_form.description_placeholder'])}
94
- rows="3"
95
- ></textarea>
96
- </div>
97
-
98
- <div class="form-row">
70
+ <div class="product-form-shell">
71
+ <Form onsubmit={handleSubmit} class="product-form">
99
72
  <div class="form-group">
100
- <label for="price">Price *</label>
101
- <input
102
- id="price"
103
- type="number"
104
- step="0.01"
105
- min="0"
106
- bind:value={formData.price}
73
+ <label for="name">{t(M['products.product_form.name_label'])}</label>
74
+ <Input
75
+ id="name"
76
+ type="text"
77
+ bind:value={formData.name}
107
78
  disabled={loading}
108
- class="form-input"
109
- class:error={errors.price}
110
- placeholder="0.00"
79
+ class={errors.name ? 'error' : ''}
80
+ placeholder={t(M['products.product_form.name_placeholder'])}
111
81
  />
112
- {#if errors.price}
113
- <span class="error-message">{errors.price}</span>
82
+ {#if errors.name}
83
+ <span class="error-message">{errors.name}</span>
114
84
  {/if}
115
85
  </div>
116
86
 
117
87
  <div class="form-group">
118
- <label for="category">Category</label>
119
- <input
120
- id="category"
121
- type="text"
122
- bind:value={formData.category}
88
+ <label for="description">Description</label>
89
+ <Textarea
90
+ id="description"
91
+ bind:value={formData.description}
123
92
  disabled={loading}
124
- class="form-input"
125
- placeholder={t(M['products.product_form.category_placeholder'])}
126
- />
93
+ placeholder={t(M['products.product_form.description_placeholder'])}
94
+ rows={3}
95
+ ></Textarea>
96
+ </div>
97
+
98
+ <div class="form-row">
99
+ <div class="form-group">
100
+ <label for="price">Price *</label>
101
+ <Input
102
+ id="price"
103
+ type="number"
104
+ step="0.01"
105
+ min="0"
106
+ bind:value={formData.price}
107
+ disabled={loading}
108
+ class={errors.price ? 'error' : ''}
109
+ placeholder="0.00"
110
+ />
111
+ {#if errors.price}
112
+ <span class="error-message">{errors.price}</span>
113
+ {/if}
114
+ </div>
115
+
116
+ <div class="form-group">
117
+ <label for="category">Category</label>
118
+ <Input
119
+ id="category"
120
+ type="text"
121
+ bind:value={formData.category}
122
+ disabled={loading}
123
+ placeholder={t(M['products.product_form.category_placeholder'])}
124
+ />
125
+ </div>
127
126
  </div>
128
- </div>
129
-
130
- <div class="form-group">
131
- <label for="tags">Tags</label>
132
- <input
133
- id="tags"
134
- type="text"
135
- bind:value={formData.tags}
136
- disabled={loading}
137
- class="form-input"
138
- placeholder={t(M['products.product_form.tags_placeholder'])}
139
- />
140
- <small class="form-hint">{t(M['products.product_form.tags_hint'])}</small>
141
- </div>
142
-
143
- <div class="form-group">
144
- <label class="checkbox-label">
145
- <input
146
- type="checkbox"
147
- bind:checked={formData.inStock}
127
+
128
+ <div class="form-group">
129
+ <label for="tags">Tags</label>
130
+ <Input
131
+ id="tags"
132
+ type="text"
133
+ bind:value={formData.tags}
148
134
  disabled={loading}
149
- class="form-checkbox"
135
+ placeholder={t(M['products.product_form.tags_placeholder'])}
150
136
  />
151
- {t(M['products.product_form.in_stock_label'])}
152
- </label>
153
- </div>
154
-
155
- <div class="form-actions">
156
- {#if onCancel}
157
- <Button type="button" variant="secondary" onclick={onCancel} disabled={loading}>
158
- Cancel
159
- </Button>
160
- {/if}
137
+ <small class="form-hint">{t(M['products.product_form.tags_hint'])}</small>
138
+ </div>
161
139
 
162
- <Button type="submit" variant="primary" disabled={loading}>
163
- {#if loading}
164
- Saving...
165
- {:else}
166
- {product.id ? 'Update Product' : 'Create Product'}
140
+ <div class="form-group">
141
+ <label class="checkbox-label">
142
+ <!-- raw-primitive-allow: native checkbox; no Provider-free checkbox primitive (Toggle is a switch with different semantics, CheckboxInput requires a Provider) -->
143
+ <input
144
+ type="checkbox"
145
+ bind:checked={formData.inStock}
146
+ disabled={loading}
147
+ class="form-checkbox"
148
+ />
149
+ {t(M['products.product_form.in_stock_label'])}
150
+ </label>
151
+ </div>
152
+
153
+ <div class="form-actions">
154
+ {#if onCancel}
155
+ <Button type="button" variant="secondary" onclick={onCancel} disabled={loading}>
156
+ Cancel
157
+ </Button>
167
158
  {/if}
168
- </Button>
169
- </div>
170
- </form>
159
+
160
+ <Button type="submit" variant="primary" disabled={loading}>
161
+ {#if loading}
162
+ Saving...
163
+ {:else}
164
+ {product.id ? 'Update Product' : 'Create Product'}
165
+ {/if}
166
+ </Button>
167
+ </div>
168
+ </Form>
169
+ </div>
171
170
 
172
171
  <style>
173
- .product-form {
172
+ .product-form-shell :global(.product-form) {
174
173
  max-width: 500px;
175
174
  padding: 1.5rem;
176
175
  background: var(--smrt-color-surface, #fff);
177
176
  border-radius: var(--smrt-radius-md, 8px);
178
177
  border: 1px solid var(--smrt-color-outline-variant, #e2e8f0);
179
178
  }
180
-
179
+
181
180
  .form-group {
182
181
  margin-bottom: 1rem;
183
182
  }
184
-
183
+
185
184
  .form-row {
186
185
  display: grid;
187
186
  grid-template-columns: 1fr 1fr;
188
187
  gap: 1rem;
189
188
  }
190
-
189
+
191
190
  label {
192
191
  display: block;
193
192
  margin-bottom: 0.25rem;
@@ -196,30 +195,13 @@ function handleSubmit(event: Event) {
196
195
  font-size: var(--smrt-typography-label-large-size, 0.875rem);
197
196
  }
198
197
 
199
- .form-input, .form-textarea {
200
- width: 100%;
201
- padding: 0.5rem;
202
- border: 1px solid var(--smrt-color-outline-variant, #d1d5db);
203
- border-radius: var(--smrt-radius-sm, 4px);
204
- font-size: var(--smrt-typography-body-medium-size, 0.875rem);
205
- transition: border-color 0.2s;
206
- }
207
-
208
- .form-input:focus, .form-textarea:focus {
209
- outline: none;
210
- border-color: var(--smrt-color-primary, #3b82f6);
211
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--smrt-color-primary, #3b82f6) 10%, transparent);
212
- }
213
-
214
- .form-input.error {
198
+ /* Error border on the migrated <Input>. The primitive renders the inner
199
+ <input class="input error"> inside its own component, so the scoped class
200
+ can't reach it without :global (#1589). */
201
+ .product-form-shell :global(.input.error) {
215
202
  border-color: var(--smrt-color-error, #dc2626);
216
203
  }
217
-
218
- .form-textarea {
219
- resize: vertical;
220
- min-height: 80px;
221
- }
222
-
204
+
223
205
  .checkbox-label {
224
206
  display: flex;
225
207
  align-items: center;
@@ -1 +1 @@
1
- {"version":3,"file":"ProductForm.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/ProductForm.svelte"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAG5C,UAAU,KAAK;IACb,OAAO,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;IAC/B,QAAQ,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC;IAClD,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AA2HD,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"ProductForm.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/ProductForm.svelte"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAG5C,UAAU,KAAK;IACb,OAAO,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;IAC/B,QAAQ,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC;IAClD,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AA+HD,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -4,6 +4,7 @@
4
4
  * Demonstrates "Define Once, Consume Everywhere" - form is generated from Product class definition
5
5
  */
6
6
 
7
+ import { Form } from '@happyvertical/smrt-ui/forms';
7
8
  import { useI18n } from '@happyvertical/smrt-ui/i18n';
8
9
  import { Button } from '@happyvertical/smrt-ui/ui';
9
10
  import { M } from '../../i18n.js';
@@ -113,31 +114,33 @@ function _getFieldType(
113
114
  </div>
114
115
  </header>
115
116
 
116
- <form class="form-content" onsubmit={handleSubmit}>
117
- {#each fieldSchema as field}
118
- <FieldRenderer
119
- fieldName={field.name}
120
- fieldType={field.type}
121
- value={formData[field.name]}
122
- label={field.label}
123
- placeholder={field.placeholder}
124
- required={field.required || false}
125
- {readonly}
126
- onUpdate={(value) => updateField(field.name, value)}
127
- />
128
- {/each}
129
-
130
- {#if !readonly}
131
- <div class="form-actions">
132
- <Button type="submit" variant="primary">
133
- {submitLabel}
134
- </Button>
135
- <Button type="button" variant="secondary" onclick={() => formData = {}}>
136
- Reset
137
- </Button>
138
- </div>
139
- {/if}
140
- </form>
117
+ <div class="form-content-shell">
118
+ <Form class="form-content" onsubmit={handleSubmit}>
119
+ {#each fieldSchema as field}
120
+ <FieldRenderer
121
+ fieldName={field.name}
122
+ fieldType={field.type}
123
+ value={formData[field.name]}
124
+ label={field.label}
125
+ placeholder={field.placeholder}
126
+ required={field.required || false}
127
+ {readonly}
128
+ onUpdate={(value) => updateField(field.name, value)}
129
+ />
130
+ {/each}
131
+
132
+ {#if !readonly}
133
+ <div class="form-actions">
134
+ <Button type="submit" variant="primary">
135
+ {submitLabel}
136
+ </Button>
137
+ <Button type="button" variant="secondary" onclick={() => formData = {}}>
138
+ Reset
139
+ </Button>
140
+ </div>
141
+ {/if}
142
+ </Form>
143
+ </div>
141
144
 
142
145
  <!-- Demo: Show current form state -->
143
146
  <details class="form-debug">
@@ -174,7 +177,7 @@ function _getFieldType(
174
177
  font-style: italic;
175
178
  }
176
179
 
177
- .form-content {
180
+ .form-content-shell :global(.form-content) {
178
181
  display: flex;
179
182
  flex-direction: column;
180
183
  gap: 1rem;
@@ -1 +1 @@
1
- {"version":3,"file":"AutoForm.svelte.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/auto-generated/AutoForm.svelte.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAI/C,UAAU,KAAK;IACb,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,CAAC;IACvC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,CAAC;CACxC;AAoID,QAAA,MAAM,QAAQ,2CAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
1
+ {"version":3,"file":"AutoForm.svelte.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/auto-generated/AutoForm.svelte.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAI/C,UAAU,KAAK;IACb,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,CAAC;IACvC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,CAAC;CACxC;AAuID,QAAA,MAAM,QAAQ,2CAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
@@ -4,6 +4,7 @@
4
4
  * This demonstrates the "Define Once, Consume Everywhere" vision
5
5
  */
6
6
 
7
+ import { Input, Textarea } from '@happyvertical/smrt-ui/forms';
7
8
  import { useI18n } from '@happyvertical/smrt-ui/i18n';
8
9
  import { M } from '../../i18n.js';
9
10
 
@@ -32,12 +33,13 @@ const {
32
33
  }: Props = $props();
33
34
 
34
35
  // Auto-generate label from field name if not provided
35
- const displayLabel =
36
+ const displayLabel = $derived(
36
37
  label ||
37
- fieldName
38
- .replace(/([A-Z])/g, ' $1')
39
- .replace(/^./, (str) => str.toUpperCase());
40
- const fieldId = `field-${fieldName}`;
38
+ fieldName
39
+ .replace(/([A-Z])/g, ' $1')
40
+ .replace(/^./, (str) => str.toUpperCase()),
41
+ );
42
+ const fieldId = $derived(`field-${fieldName}`);
41
43
 
42
44
  function handleUpdate(newValue: any) {
43
45
  if (onUpdate && !readonly) {
@@ -92,10 +94,9 @@ function handleObjectInput(event: Event) {
92
94
  </label>
93
95
 
94
96
  {#if fieldType === 'string'}
95
- <input
97
+ <Input
96
98
  id={fieldId}
97
99
  type="text"
98
- class="field-input"
99
100
  {value}
100
101
  {placeholder}
101
102
  {readonly}
@@ -103,10 +104,9 @@ function handleObjectInput(event: Event) {
103
104
  oninput={handleStringInput}
104
105
  />
105
106
  {:else if fieldType === 'number'}
106
- <input
107
+ <Input
107
108
  id={fieldId}
108
109
  type="number"
109
- class="field-input"
110
110
  value={value || 0}
111
111
  {placeholder}
112
112
  {readonly}
@@ -114,6 +114,7 @@ function handleObjectInput(event: Event) {
114
114
  oninput={handleNumberInput}
115
115
  />
116
116
  {:else if fieldType === 'boolean'}
117
+ <!-- raw-primitive-allow: native checkbox; no Provider-free checkbox primitive (Toggle is a switch with different semantics, CheckboxInput requires a Provider) -->
117
118
  <input
118
119
  id={fieldId}
119
120
  type="checkbox"
@@ -123,26 +124,24 @@ function handleObjectInput(event: Event) {
123
124
  onchange={handleBooleanInput}
124
125
  />
125
126
  {:else if fieldType === 'array'}
126
- <textarea
127
+ <Textarea
127
128
  id={fieldId}
128
- class="field-textarea"
129
129
  value={Array.isArray(value) ? value.join(', ') : ''}
130
130
  placeholder={placeholder || 'Enter comma-separated values'}
131
131
  {readonly}
132
132
  {required}
133
133
  oninput={handleArrayInput}
134
- />
134
+ ></Textarea>
135
135
  <div class="field-hint">{t(M['products.field_renderer.array_hint'])}</div>
136
136
  {:else if fieldType === 'object'}
137
- <textarea
137
+ <Textarea
138
138
  id={fieldId}
139
- class="field-textarea"
140
139
  value={typeof value === 'object' ? JSON.stringify(value, null, 2) : '{}'}
141
140
  placeholder={placeholder || 'Enter JSON object'}
142
141
  {readonly}
143
142
  {required}
144
143
  oninput={handleObjectInput}
145
- />
144
+ ></Textarea>
146
145
  <div class="field-hint">{t(M['products.field_renderer.object_hint'])}</div>
147
146
  {/if}
148
147
  </div>
@@ -165,27 +164,6 @@ function handleObjectInput(event: Event) {
165
164
  color: var(--smrt-color-error, #dc2626);
166
165
  }
167
166
 
168
- .field-input,
169
- .field-textarea {
170
- padding: 0.5rem 0.75rem;
171
- border: 1px solid var(--smrt-color-outline-variant, #d1d5db);
172
- border-radius: var(--smrt-radius-sm, 0.375rem);
173
- font-size: var(--smrt-typography-body-medium-size, 0.875rem);
174
- transition: border-color 0.2s, box-shadow 0.2s;
175
- }
176
-
177
- .field-input:focus,
178
- .field-textarea:focus {
179
- outline: none;
180
- border-color: var(--smrt-color-primary, #3b82f6);
181
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--smrt-color-primary, #3b82f6) 10%, transparent);
182
- }
183
-
184
- .field-textarea {
185
- min-height: 4rem;
186
- resize: vertical;
187
- }
188
-
189
167
  .field-checkbox {
190
168
  width: 1rem;
191
169
  height: 1rem;
@@ -196,10 +174,4 @@ function handleObjectInput(event: Event) {
196
174
  color: var(--smrt-color-on-surface-variant, #6b7280);
197
175
  font-style: italic;
198
176
  }
199
-
200
- .field-input:read-only,
201
- .field-textarea:read-only {
202
- background-color: var(--smrt-color-surface-container-low, #f9fafb);
203
- cursor: not-allowed;
204
- }
205
177
  </style>
@@ -1 +1 @@
1
- {"version":3,"file":"FieldRenderer.svelte.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/auto-generated/FieldRenderer.svelte.ts"],"names":[],"mappings":"AAWA,UAAU,KAAK;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;IAChE,KAAK,EAAE,GAAG,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,CAAC;CACjC;AAmGD,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"FieldRenderer.svelte.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/auto-generated/FieldRenderer.svelte.ts"],"names":[],"mappings":"AAYA,UAAU,KAAK;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;IAChE,KAAK,EAAE,GAAG,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,CAAC;CACjC;AAsGD,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { Input, Select } from '@happyvertical/smrt-ui/forms';
2
3
  import { useI18n } from '@happyvertical/smrt-ui/i18n';
3
4
  import { Button } from '@happyvertical/smrt-ui/ui';
4
5
  import { onMount } from 'svelte';
@@ -103,20 +104,20 @@ function handleCancelForm() {
103
104
 
104
105
  <div class="catalog-controls">
105
106
  <div class="search-filters">
106
- <input
107
+ <Input
107
108
  type="text"
108
109
  bind:value={searchQuery}
109
110
  placeholder={t(M['products.product_catalog.search_placeholder'])}
110
111
  aria-label={t(M['products.product_catalog.search_placeholder'])}
111
112
  class="search-input"
112
113
  />
113
-
114
- <select bind:value={selectedCategory} class="category-filter">
114
+
115
+ <Select bind:value={selectedCategory} class="category-filter">
115
116
  <option value="">{t(M['products.product_catalog.all_categories'])}</option>
116
117
  {#each productStore.categories as category}
117
118
  <option value={category}>{category}</option>
118
119
  {/each}
119
- </select>
120
+ </Select>
120
121
  </div>
121
122
 
122
123
  {#if !readonly && (showCreateForm || productStore.items.length === 0)}
@@ -224,20 +225,17 @@ function handleCancelForm() {
224
225
  gap: 0.75rem;
225
226
  flex: 1;
226
227
  }
227
-
228
- .search-input, .category-filter {
229
- padding: 0.5rem;
230
- border: 1px solid var(--smrt-color-outline-variant, #d1d5db);
231
- border-radius: var(--smrt-radius-sm, 4px);
232
- font-size: var(--smrt-typography-body-medium-size, 0.875rem);
233
- }
234
-
235
- .search-input {
228
+
229
+ /* Layout sizing for the migrated <Input>/<Select>. The primitives render the
230
+ inner element with the forwarded class, so pierce with :global to keep the
231
+ flex sizing the old scoped rules supplied (#1589); padding/border/font come
232
+ from the primitives' tokenised styling now. */
233
+ .search-filters :global(.search-input) {
236
234
  flex: 1;
237
235
  max-width: 300px;
238
236
  }
239
-
240
- .category-filter {
237
+
238
+ .search-filters :global(.category-filter) {
241
239
  min-width: 150px;
242
240
  }
243
241
 
@@ -1 +1 @@
1
- {"version":3,"file":"ProductCatalog.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/features/ProductCatalog.svelte"],"names":[],"mappings":"AAaA,UAAU,KAAK;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAoKD,QAAA,MAAM,cAAc,2CAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
1
+ {"version":3,"file":"ProductCatalog.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/features/ProductCatalog.svelte"],"names":[],"mappings":"AAcA,UAAU,KAAK;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAqKD,QAAA,MAAM,cAAc,2CAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "version": "1.0.0",
3
- "timestamp": 1782278452967,
3
+ "timestamp": 1782318262725,
4
4
  "packageName": "@happyvertical/smrt-products",
5
- "packageVersion": "0.34.6",
5
+ "packageVersion": "0.34.8",
6
6
  "objects": {
7
7
  "@happyvertical/smrt-products:CategoryCollection": {
8
8
  "name": "categorycollection",
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-06-24T05:20:53.581Z",
3
+ "generatedAt": "2026-06-24T16:24:23.349Z",
4
4
  "packageName": "@happyvertical/smrt-products",
5
- "packageVersion": "0.34.6",
5
+ "packageVersion": "0.34.8",
6
6
  "sourceManifestPath": "dist/lib/manifest.json",
7
7
  "agentDocPath": "AGENTS.md",
8
8
  "sourceHashes": {
9
- "manifest": "584ad2da857b7180b8a585c41e0975b7743a07f6ad3ffc172186e396cd814cb7",
10
- "packageJson": "5a90519cc03f1b0745852b4c8b691d0a03199722ab64cdf53425665e20be1ec5",
11
- "agents": "4ca8837df73cf8b966b65cea639a058baa8f92ae536f3cd5036546fcbfeb37c8"
9
+ "manifest": "d88ddc519370ba8d1e115d9e74ca09fc6fc7908bdf17894ae9edb7db1e56517c",
10
+ "packageJson": "b1dea34106ce986acebde25be38c7e1aa7243ab14170453a71a756f6e16031a4",
11
+ "agents": "ede6063b72600b84908e1e32216fbbdd69d70d309f0b23c2add338ad080702d4"
12
12
  },
13
13
  "exports": [
14
14
  ".",
@@ -1572,5 +1572,5 @@
1572
1572
  "polymorphicAssociations": 0,
1573
1573
  "uuidColumns": 21
1574
1574
  },
1575
- "agentDoc": "# @happyvertical/smrt-products\n\nProduct catalog — reference template for triple-consumption: npm package library, module federation, and standalone REST API server.\n\n## Models\n\n- **Product**: STI base. Tenant-scoped (`@TenantScoped({ mode: 'optional' })`, nullable `tenantId`). Knowledge base product with specs, tags, and the `productType` discriminator. Consumers subclass this with vertical-specific subtypes (apparel `Style`, automotive `Model`, furniture `Design`, etc.).\n- **Material**: STI subtype. Raw input consumed by manufacturing (fabric, trim, thread, packaging, component). Meta fields: `materialKind`, `uom`, `costPerUnit`. Materials are first-class products in the catalog — the SAP/NetSuite pattern. Bills of materials in `@happyvertical/smrt-manufacturing` reference Materials by id.\n- **ProductVariant**: standalone (NOT a Product STI subtype). Declarative axis definition: `productId`, `axisName` (e.g. `'size'`, `'color'`, `'finish'`), `allowedValues` (JSON-stored array), optional `label` and `sortOrder`. Per-SKU value pins live on `Sku.attributes`. Lives in its own `product_variants` table; `conflictColumns: ['product_id', 'axis_name', 'tenant_id']`.\n- **Sku**: standalone. The smallest sellable / countable unit. `productId` points at a `Product` or any of its STI subtypes; `code` is the human-meaningful identifier; `attributes` JSON pins each axis value (`{ size: 'M', color: 'navy' }`). Lives in its own `product_skus` table; `conflictColumns: ['code', 'tenant_id']`. Stock balance and movement history for a Sku live in `@happyvertical/smrt-inventory`.\n- **ProductAsset**: dedicated owned-asset join in `product_assets` with `relationship` and `sortOrder`. Tenant-scoped to match Product.\n- **Category**: hierarchical (parentId, level, productCount). STI base, tenant-scoped.\n\n## Vertical-specific Product subtypes\n\nThis package deliberately ships ONLY the generic primitives. Domain-specific top-level item types live in the consumer's template:\n\n- Apparel: `Style extends Product`, `Makeup extends Product`\n- Furniture: `Design extends Product`, `Configuration extends Product`\n- Automotive: `Model extends Product`, `Trim extends Product`\n- CPG: `Brand extends Product`, `Recipe extends Product`\n\nEach is a small subclass: `@smrt()`, override `productType`, add `@meta()` fields. See `packages/template-apparel-erp` for a worked example.\n\n## Variants — the two concepts\n\nThe framework's variant story uses two distinct primitives that look similar but do different jobs:\n\n- **`ProductVariant`** — *axis declaration*. \"For product X, axis `size` accepts the values `[XS, S, M, L, XL]`.\" Drives form/UI choices.\n- **`Sku.attributes`** — *per-unit value pins*. `{ color: 'navy', size: 'M' }` on each concrete sellable SKU.\n\nBoth live in this package — all catalog shapes are here. Stock motion (where the SKU is, how many, history) lives in `@happyvertical/smrt-inventory`.\n\nThere is deliberately no separate \"catalog grouping above SKU\" row (a la a \"Navy colorway\" row sitting between the Product and its Skus). Group SKUs by axis value via `attributes.color = 'navy'` queries; attach per-axis-value assets via `ProductAsset` rows with relationship metadata. Matches how Shopify, Stripe Products, and most e-commerce platforms model the same shape.\n\n## Collections\n\n- **ProductCollection** — base. Polymorphic queries return the correct subclass instance per row.\n- **MaterialCollection** — STI-filtered subclass. Override `_itemClass`; framework auto-filters by `_meta_type`.\n- **ProductVariantCollection** — standalone collection over `product_variants`. Helpers: `findForProduct(productId)`, `findAxis(productId, axisName)`.\n- **SkuCollection** — standalone collection over `product_skus`. Helpers: `findByCode`, `findByBarcode`, `findByProduct`, `findByParent`, `findActive`.\n- **CategoryCollection** — standard Category CRUD with `getRootCategories()`.\n- **ProductAssetCollection** — direct access to `product_assets` rows and product asset helper wrappers.\n\n## Multi-tenancy\n\nOptional. With `withTenant(id, fn)` (from `@happyvertical/smrt-tenancy`), all queries auto-filter and inserts auto-stamp `tenantId`. Without it, models behave globally (`tenantId = null`). This lets the same package serve both shared reference catalogs and per-merchant SaaS catalogs.\n\n## Triple-Consumption Pattern\n\nSame codebase consumed three ways:\n1. **NPM library**: import classes directly\n2. **Module federation**: runtime component sharing (experimental)\n3. **Standalone API**: `startRestServer([Product, Category])`\n\n## Virtual Modules (Vite)\n\nAuto-generated via Vite plugins: `@happyvertical/smrt-client` (TypeScript client), `@happyvertical/smrt-types`, `@happyvertical/smrt-routes` (Express), `@happyvertical/smrt-mcp`, `@happyvertical/smrt-manifest`.\n\nSvelte 5 stores use runes (`$state`, `$derived`, `$effect`). Separate `product-store.server.svelte.ts` vs `product-store.client.svelte.ts` for SSR safety.\n\n## Schema migrations (Phase 1 release)\n\nThis package's schema changed shape between the previous release and the Phase 1 apparel-ERP release. Consumers upgrading need to migrate two tables.\n\n### `Sku` rows moved tables\n\nPreviously `Sku` shipped in `@happyvertical/smrt-inventory` under table `inventory_skus`. It now lives here under `product_skus`. Cross-package refs (`StockLevel.skuId`, `BomLine.componentSkuId`) carry plain string ids that still resolve.\n\n**Upgrade procedure** (works on SQLite + Postgres; does NOT rely on `CREATE TABLE AS` which strips constraints):\n\n1. **Boot the new version once** so the framework's lazy `syncSchema` creates `product_skus` with the right PRIMARY KEY, NOT NULL, UNIQUE (`code`, `tenant_id`), and indexes derived from the `Sku` model.\n\n2. **Idempotently copy rows**:\n\n ```sql\n BEGIN;\n INSERT INTO product_skus (\n id, slug, context, created_at, updated_at,\n tenant_id, product_id, code, barcode, name,\n attributes, weight_grams, parent_sku_id, active\n )\n SELECT\n id, slug, context, created_at, updated_at,\n tenant_id, product_id, code, barcode, name,\n attributes, weight_grams, parent_sku_id, active\n FROM inventory_skus\n WHERE NOT EXISTS (\n SELECT 1 FROM product_skus p WHERE p.id = inventory_skus.id\n );\n COMMIT;\n ```\n\n3. **Drop the legacy table** once row counts match:\n\n ```sql\n DROP TABLE IF EXISTS inventory_skus;\n ```\n\n### `ProductVariant` changed shape entirely\n\nPreviously a Product STI subtype carrying `parentProductId` + `axisValues` JSON inside `_meta_data` on the shared `products` table (`_meta_type='@happyvertical/smrt-products:ProductVariant'`). It is now a **standalone model** on its own `product_variants` table, with columns `productId`, `axisName`, `allowedValues`, `label`, `sortOrder`.\n\n**There is no automatic data conversion.** The old \"catalog grouping above SKU\" concept doesn't map 1:1 to the new \"per-axis declaration\" concept. Recommended procedure:\n\n1. Inspect the old rows: `SELECT * FROM products WHERE _meta_type = '@happyvertical/smrt-products:ProductVariant';`. Treat them as historical reference.\n2. Re-author axis declarations against the new shape (one `ProductVariant` row per `(productId, axisName)` pair, with `allowedValues` listing the values).\n3. Once the new declarations are populated and verified, remove the legacy rows: `DELETE FROM products WHERE _meta_type = '@happyvertical/smrt-products:ProductVariant';`.\n\nIf you had per-colorway / per-variant images attached via `ProductAsset` rows pointing at old ProductVariant ids, repoint those to the parent Product id; group-by-axis-value queries on `Sku.attributes` cover the same use case at the SKU level.\n\n## Gotchas\n\n- **`ProductVariant` and `Sku` are NOT Product STI subtypes** — they each have their own table (`product_variants`, `product_skus`) because their shapes don't fit the Product schema. Don't try to query them via `ProductCollection`.\n- **`npm run build` emits the published library surface directly**: package consumers read model, collection, and helper exports from `dist/lib`. Cross-package imports from this package should target `/models` or `/collections` subpaths (not the main entry) when the consumer isn't a vite app — the main entry pulls in vite virtual modules.\n- **Use `npm run build:all` only when you need standalone or federation bundles** in addition to the library output\n- **Constructor must explicitly assign all properties**: `Object.assign` doesn't work reliably with decorators\n- **STI subtype-specific fields use `Meta<T>`** — declare them as `fieldName: Meta<FieldType> = defaultValue`. The AST scanner detects the `Meta<T>` type wrapper at build time and routes the field through `_meta_data` JSON storage instead of materializing it as a column on the parent's table. Do **NOT** use the runtime `@meta()` decorator on STI children — it never reaches the manifest, so the schema generator treats the field as an ordinary column on the parent table and the framework's hydration path can't tell it's meta. Override `productType` on each subclass.\n- **Tenant-scoped STI children must repeat `@TenantScoped`** — `@TenantScoped` registers per concrete className, so `Material extends Product` inheriting from a tenant-scoped `Product` is NOT automatically tenant-scoped itself. `MaterialCollection.list/save` passes `'Material'` to the tenant interceptor, which looks up `'Material'` (not `'Product'`) in the per-class registry. Without an explicit `@TenantScoped({ mode: 'optional' })` on `Material`, material rows skip the tenant auto-filter and auto-populate.\n- **STI children must repeat `@smrt({ api, mcp, cli })` generation config (S5 #1406)** — like `@TenantScoped`, the generation config is registered per concrete className and is NOT inherited from the STI parent. An empty `@smrt()` on a child resolves `getConfig(child).api/.mcp` to `undefined`, which the REST and MCP generators treat as \"expose EVERYTHING\" — including `delete` and write-capable MCP tools — even when the parent deliberately restricts its surface. The package's `mcp.ts` generator enumerates the whole registry, so an under-configured child ships a wide-open surface silently. Re-declare the parent's `api`/`mcp`/`cli` posture on every subtype (see `Material`). Consumer subtypes (apparel `Style`/`Makeup`, automotive `Model`/`Trim`, …) must do the same.\n- **Two-tenant same-slug is currently a hard error** for tenant-scoped STI bases (`Product`, `Category`). The core schema generator hardcodes the STI unique index as `(slug, context, _meta_type)` and does NOT include `tenant_id` even for `@TenantScoped` classes. Two tenants saving a row with the same slug + context + meta_type collide at the SQL layer. Workaround: namespace slugs per tenant on the application side (e.g. `${tenantId}-widget`) until the upstream framework fix lands.\n- **Module Federation marked experimental**: may change\n"
1575
+ "agentDoc": "# @happyvertical/smrt-products\n\nProduct catalog — reference template for triple-consumption: npm package library, module federation, and standalone REST API server.\n\n## Models\n\n- **Product**: STI base. Tenant-scoped (`@TenantScoped({ mode: 'optional' })`, nullable `tenantId`). Knowledge base product with specs, tags, and the `productType` discriminator. Consumers subclass this with vertical-specific subtypes (apparel `Style`, automotive `Model`, furniture `Design`, etc.).\n- **Material**: STI subtype. Raw input consumed by manufacturing (fabric, trim, thread, packaging, component). Meta fields: `materialKind`, `uom`, `costPerUnit`. Materials are first-class products in the catalog — the SAP/NetSuite pattern. Bills of materials in `@happyvertical/smrt-manufacturing` reference Materials by id.\n- **ProductVariant**: standalone (NOT a Product STI subtype). Declarative axis definition: `productId`, `axisName` (e.g. `'size'`, `'color'`, `'finish'`), `allowedValues` (JSON-stored array), optional `label` and `sortOrder`. Per-SKU value pins live on `Sku.attributes`. Lives in its own `product_variants` table; `conflictColumns: ['product_id', 'axis_name', 'tenant_id']`.\n- **Sku**: standalone. The smallest sellable / countable unit. `productId` points at a `Product` or any of its STI subtypes; `code` is the human-meaningful identifier; `attributes` JSON pins each axis value (`{ size: 'M', color: 'navy' }`). Lives in its own `product_skus` table; `conflictColumns: ['code', 'tenant_id']`. Stock balance and movement history for a Sku live in `@happyvertical/smrt-inventory`.\n- **ProductAsset**: dedicated owned-asset join in `product_assets` with `relationship` and `sortOrder`. Tenant-scoped to match Product.\n- **Category**: hierarchical (parentId, level, productCount). STI base, tenant-scoped.\n\n## Vertical-specific Product subtypes\n\nThis package deliberately ships ONLY the generic primitives. Domain-specific top-level item types live in the consumer's template:\n\n- Apparel: `Style extends Product`, `Makeup extends Product`\n- Furniture: `Design extends Product`, `Configuration extends Product`\n- Automotive: `Model extends Product`, `Trim extends Product`\n- CPG: `Brand extends Product`, `Recipe extends Product`\n\nEach is a small subclass: `@smrt()`, override `productType`, add `@meta()` fields. See `packages/template-apparel-erp` for a worked example.\n\n## Variants — the two concepts\n\nThe framework's variant story uses two distinct primitives that look similar but do different jobs:\n\n- **`ProductVariant`** — *axis declaration*. \"For product X, axis `size` accepts the values `[XS, S, M, L, XL]`.\" Drives form/UI choices.\n- **`Sku.attributes`** — *per-unit value pins*. `{ color: 'navy', size: 'M' }` on each concrete sellable SKU.\n\nBoth live in this package — all catalog shapes are here. Stock motion (where the SKU is, how many, history) lives in `@happyvertical/smrt-inventory`.\n\nThere is deliberately no separate \"catalog grouping above SKU\" row (a la a \"Navy colorway\" row sitting between the Product and its Skus). Group SKUs by axis value via `attributes.color = 'navy'` queries; attach per-axis-value assets via `ProductAsset` rows with relationship metadata. Matches how Shopify, Stripe Products, and most e-commerce platforms model the same shape.\n\n## Collections\n\n- **ProductCollection** — base. Polymorphic queries return the correct subclass instance per row.\n- **MaterialCollection** — STI-filtered subclass. Override `_itemClass`; framework auto-filters by `_meta_type`.\n- **ProductVariantCollection** — standalone collection over `product_variants`. Helpers: `findForProduct(productId)`, `findAxis(productId, axisName)`.\n- **SkuCollection** — standalone collection over `product_skus`. Helpers: `findByCode`, `findByBarcode`, `findByProduct`, `findByParent`, `findActive`.\n- **CategoryCollection** — standard Category CRUD with `getRootCategories()`.\n- **ProductAssetCollection** — direct access to `product_assets` rows and product asset helper wrappers.\n\n## Multi-tenancy\n\nOptional. With `withTenant(id, fn)` (from `@happyvertical/smrt-tenancy`), all queries auto-filter and inserts auto-stamp `tenantId`. Without it, models behave globally (`tenantId = null`). This lets the same package serve both shared reference catalogs and per-merchant SaaS catalogs.\n\n## Triple-Consumption Pattern\n\nSame codebase consumed three ways:\n1. **NPM library**: import classes directly\n2. **Module federation**: runtime component sharing (experimental)\n3. **Standalone API**: `startRestServer([Product, Category])`\n\n## Virtual Modules (Vite)\n\nAuto-generated via Vite plugins: `@happyvertical/smrt-client` (TypeScript client), `@happyvertical/smrt-types`, `@happyvertical/smrt-routes` (Express), `@happyvertical/smrt-mcp`, `@happyvertical/smrt-manifest`.\n\nSvelte 5 stores use runes (`$state`, `$derived`, `$effect`). `product-store.svelte.ts` is the main store (backed by the SMRT client); `product-store.client.svelte.ts` is a virtual-module-free variant for federation builds.\n\n## Schema migrations (Phase 1 release)\n\nThis package's schema changed shape between the previous release and the Phase 1 apparel-ERP release. Consumers upgrading need to migrate two tables.\n\n### `Sku` rows moved tables\n\nPreviously `Sku` shipped in `@happyvertical/smrt-inventory` under table `inventory_skus`. It now lives here under `product_skus`. Cross-package refs (`StockLevel.skuId`, `BomLine.componentSkuId`) carry plain string ids that still resolve.\n\n**Upgrade procedure** (works on SQLite + Postgres; does NOT rely on `CREATE TABLE AS` which strips constraints):\n\n1. **Boot the new version once** so the framework's lazy `syncSchema` creates `product_skus` with the right PRIMARY KEY, NOT NULL, UNIQUE (`code`, `tenant_id`), and indexes derived from the `Sku` model.\n\n2. **Idempotently copy rows**:\n\n ```sql\n BEGIN;\n INSERT INTO product_skus (\n id, slug, context, created_at, updated_at,\n tenant_id, product_id, code, barcode, name,\n attributes, weight_grams, parent_sku_id, active\n )\n SELECT\n id, slug, context, created_at, updated_at,\n tenant_id, product_id, code, barcode, name,\n attributes, weight_grams, parent_sku_id, active\n FROM inventory_skus\n WHERE NOT EXISTS (\n SELECT 1 FROM product_skus p WHERE p.id = inventory_skus.id\n );\n COMMIT;\n ```\n\n3. **Drop the legacy table** once row counts match:\n\n ```sql\n DROP TABLE IF EXISTS inventory_skus;\n ```\n\n### `ProductVariant` changed shape entirely\n\nPreviously a Product STI subtype carrying `parentProductId` + `axisValues` JSON inside `_meta_data` on the shared `products` table (`_meta_type='@happyvertical/smrt-products:ProductVariant'`). It is now a **standalone model** on its own `product_variants` table, with columns `productId`, `axisName`, `allowedValues`, `label`, `sortOrder`.\n\n**There is no automatic data conversion.** The old \"catalog grouping above SKU\" concept doesn't map 1:1 to the new \"per-axis declaration\" concept. Recommended procedure:\n\n1. Inspect the old rows: `SELECT * FROM products WHERE _meta_type = '@happyvertical/smrt-products:ProductVariant';`. Treat them as historical reference.\n2. Re-author axis declarations against the new shape (one `ProductVariant` row per `(productId, axisName)` pair, with `allowedValues` listing the values).\n3. Once the new declarations are populated and verified, remove the legacy rows: `DELETE FROM products WHERE _meta_type = '@happyvertical/smrt-products:ProductVariant';`.\n\nIf you had per-colorway / per-variant images attached via `ProductAsset` rows pointing at old ProductVariant ids, repoint those to the parent Product id; group-by-axis-value queries on `Sku.attributes` cover the same use case at the SKU level.\n\n## Gotchas\n\n- **`ProductVariant` and `Sku` are NOT Product STI subtypes** — they each have their own table (`product_variants`, `product_skus`) because their shapes don't fit the Product schema. Don't try to query them via `ProductCollection`.\n- **`npm run build` emits the published library surface directly**: package consumers read model, collection, and helper exports from `dist/lib`. Cross-package imports from this package should target `/models` or `/collections` subpaths (not the main entry) when the consumer isn't a vite app — the main entry pulls in vite virtual modules.\n- **Use `npm run build:all` only when you need standalone or federation bundles** in addition to the library output\n- **Constructor must explicitly assign all properties**: `Object.assign` doesn't work reliably with decorators\n- **STI subtype-specific fields use `Meta<T>`** — declare them as `fieldName: Meta<FieldType> = defaultValue`. The AST scanner detects the `Meta<T>` type wrapper at build time and routes the field through `_meta_data` JSON storage instead of materializing it as a column on the parent's table. Do **NOT** use the runtime `@meta()` decorator on STI children — it never reaches the manifest, so the schema generator treats the field as an ordinary column on the parent table and the framework's hydration path can't tell it's meta. Override `productType` on each subclass.\n- **Tenant-scoped STI children must repeat `@TenantScoped`** — `@TenantScoped` registers per concrete className, so `Material extends Product` inheriting from a tenant-scoped `Product` is NOT automatically tenant-scoped itself. `MaterialCollection.list/save` passes `'Material'` to the tenant interceptor, which looks up `'Material'` (not `'Product'`) in the per-class registry. Without an explicit `@TenantScoped({ mode: 'optional' })` on `Material`, material rows skip the tenant auto-filter and auto-populate.\n- **STI children must repeat `@smrt({ api, mcp, cli })` generation config (S5 #1406)** — like `@TenantScoped`, the generation config is registered per concrete className and is NOT inherited from the STI parent. An empty `@smrt()` on a child resolves `getConfig(child).api/.mcp` to `undefined`, which the REST and MCP generators treat as \"expose EVERYTHING\" — including `delete` and write-capable MCP tools — even when the parent deliberately restricts its surface. The package's `mcp.ts` generator enumerates the whole registry, so an under-configured child ships a wide-open surface silently. Re-declare the parent's `api`/`mcp`/`cli` posture on every subtype (see `Material`). Consumer subtypes (apparel `Style`/`Makeup`, automotive `Model`/`Trim`, …) must do the same.\n- **Two-tenant same-slug is currently a hard error** for tenant-scoped STI bases (`Product`, `Category`). The core schema generator hardcodes the STI unique index as `(slug, context, _meta_type)` and does NOT include `tenant_id` even for `@TenantScoped` classes. Two tenants saving a row with the same slug + context + meta_type collide at the SQL layer. Workaround: namespace slugs per tenant on the application side (e.g. `${tenantId}-widget`) until the upstream framework fix lands.\n- **Module Federation marked experimental**: may change\n"
1576
1576
  }