@happyvertical/smrt-tenancy 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/AGENTS.md +71 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/README.md +122 -0
  5. package/dist/__smrt-register__.d.ts +2 -0
  6. package/dist/__smrt-register__.d.ts.map +1 -0
  7. package/dist/adapters/cli.d.ts +178 -0
  8. package/dist/adapters/cli.d.ts.map +1 -0
  9. package/dist/adapters/express.d.ts +115 -0
  10. package/dist/adapters/express.d.ts.map +1 -0
  11. package/dist/adapters/index.d.ts +22 -0
  12. package/dist/adapters/index.d.ts.map +1 -0
  13. package/dist/adapters/index.js +7 -0
  14. package/dist/adapters/index.js.map +1 -0
  15. package/dist/adapters/sveltekit.d.ts +123 -0
  16. package/dist/adapters/sveltekit.d.ts.map +1 -0
  17. package/dist/chunks/context-B5CKsmMi.js +190 -0
  18. package/dist/chunks/context-B5CKsmMi.js.map +1 -0
  19. package/dist/chunks/sveltekit-9eRH1RLw.js +153 -0
  20. package/dist/chunks/sveltekit-9eRH1RLw.js.map +1 -0
  21. package/dist/chunks/testing-C_tV23JW.js +487 -0
  22. package/dist/chunks/testing-C_tV23JW.js.map +1 -0
  23. package/dist/context.d.ts +435 -0
  24. package/dist/context.d.ts.map +1 -0
  25. package/dist/decorators.d.ts +126 -0
  26. package/dist/decorators.d.ts.map +1 -0
  27. package/dist/enabled-state.d.ts +25 -0
  28. package/dist/enabled-state.d.ts.map +1 -0
  29. package/dist/entry-point.d.ts +83 -0
  30. package/dist/entry-point.d.ts.map +1 -0
  31. package/dist/fields.d.ts +104 -0
  32. package/dist/fields.d.ts.map +1 -0
  33. package/dist/index.d.ts +9 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +108 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/interceptor.d.ts +156 -0
  38. package/dist/interceptor.d.ts.map +1 -0
  39. package/dist/manifest.json +11 -0
  40. package/dist/playground.d.ts +2 -0
  41. package/dist/playground.d.ts.map +1 -0
  42. package/dist/playground.js +80 -0
  43. package/dist/playground.js.map +1 -0
  44. package/dist/registry.d.ts +145 -0
  45. package/dist/registry.d.ts.map +1 -0
  46. package/dist/smrt-knowledge.json +65 -0
  47. package/dist/svelte/components/TenantCard.svelte +272 -0
  48. package/dist/svelte/components/TenantCard.svelte.d.ts +18 -0
  49. package/dist/svelte/components/TenantCard.svelte.d.ts.map +1 -0
  50. package/dist/svelte/components/TenantSwitcher.svelte +68 -0
  51. package/dist/svelte/components/TenantSwitcher.svelte.d.ts +11 -0
  52. package/dist/svelte/components/TenantSwitcher.svelte.d.ts.map +1 -0
  53. package/dist/svelte/i18n.d.ts +5 -0
  54. package/dist/svelte/i18n.d.ts.map +1 -0
  55. package/dist/svelte/i18n.js +9 -0
  56. package/dist/svelte/index.d.ts +15 -0
  57. package/dist/svelte/index.d.ts.map +1 -0
  58. package/dist/svelte/index.js +19 -0
  59. package/dist/svelte/playground.d.ts +70 -0
  60. package/dist/svelte/playground.d.ts.map +1 -0
  61. package/dist/svelte/playground.js +75 -0
  62. package/dist/testing.d.ts +145 -0
  63. package/dist/testing.d.ts.map +1 -0
  64. package/dist/testing.js +11 -0
  65. package/dist/testing.js.map +1 -0
  66. package/dist/ui.d.ts +21 -0
  67. package/dist/ui.d.ts.map +1 -0
  68. package/dist/ui.js +33 -0
  69. package/dist/ui.js.map +1 -0
  70. package/package.json +99 -0
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Tenant-Scoped Class Registry
3
+ *
4
+ * Tracks which classes are tenant-scoped and their configuration.
5
+ * Used by the interceptor to determine how to handle operations.
6
+ *
7
+ * This registry supports two patterns:
8
+ * 1. @TenantScoped() decorator + tenantId field (original pattern)
9
+ * 2. @smrt({ tenantScoped: true }) in smrt-core (Issue #688 pattern)
10
+ *
11
+ * Both patterns are automatically recognized by the interceptor.
12
+ *
13
+ * @see https://github.com/happyvertical/smrt/issues/675
14
+ * @see https://github.com/happyvertical/smrt/issues/688
15
+ */
16
+ /**
17
+ * Resolved tenancy configuration for a single class, as stored in the registry.
18
+ *
19
+ * Every field has a concrete (non-optional) value — defaults are applied by
20
+ * `registerTenantScopedClass()` when the class is registered via `@TenantScoped()`.
21
+ *
22
+ * @see TenantScopedOptions
23
+ * @see registerTenantScopedClass
24
+ */
25
+ export interface TenantScopedConfig {
26
+ /**
27
+ * Tenancy mode for this class
28
+ * - 'required': Must have tenant context for all operations
29
+ * - 'optional': Works with or without tenant context
30
+ * @default 'required'
31
+ */
32
+ mode: 'required' | 'optional';
33
+ /**
34
+ * Field name containing tenant ID
35
+ * @default 'tenantId'
36
+ */
37
+ field: string;
38
+ /**
39
+ * Auto-filter all queries by tenant
40
+ * @default true
41
+ */
42
+ autoFilter: boolean;
43
+ /**
44
+ * Auto-populate tenant ID from context on create
45
+ * @default true
46
+ */
47
+ autoPopulate: boolean;
48
+ /**
49
+ * Allow super admin bypass for this class
50
+ * @default false
51
+ */
52
+ allowSuperAdminBypass: boolean;
53
+ }
54
+ /**
55
+ * Register a class as tenant-scoped with the given configuration.
56
+ *
57
+ * Called automatically by the `@TenantScoped()` decorator. You can also call
58
+ * this directly when you cannot use decorators (e.g., third-party classes or
59
+ * plain objects in tests). Defaults from `DEFAULT_CONFIG` are merged over any
60
+ * omitted options.
61
+ *
62
+ * Calling this again for the same `className` overwrites the previous entry.
63
+ *
64
+ * @param className - The class's `name` property (e.g., `'Document'`).
65
+ * @param config - Partial tenancy configuration; omitted fields receive defaults.
66
+ *
67
+ * @example
68
+ * ```typescript
69
+ * // Manually register a class (e.g., for testing)
70
+ * registerTenantScopedClass('Document', { mode: 'optional' });
71
+ * ```
72
+ *
73
+ * @see TenantScoped
74
+ * @see unregisterTenantScopedClass
75
+ */
76
+ export declare function registerTenantScopedClass(className: string, config?: Partial<TenantScopedConfig>): void;
77
+ /**
78
+ * Remove a class from the tenant-scoped registry.
79
+ *
80
+ * Primarily intended for test teardown — use `clearTenantScopedRegistry()` to
81
+ * reset the entire registry at once.
82
+ *
83
+ * @param className - The class name to remove (e.g., `'Document'`).
84
+ *
85
+ * @see clearTenantScopedRegistry
86
+ * @see registerTenantScopedClass
87
+ */
88
+ export declare function unregisterTenantScopedClass(className: string): void;
89
+ /**
90
+ * Return `true` if the named class is registered as tenant-scoped.
91
+ *
92
+ * Checks two sources in order:
93
+ * 1. The local registry populated by `@TenantScoped()`.
94
+ * 2. The core `ObjectRegistry` populated by `@smrt({ tenantScoped: true })`.
95
+ *
96
+ * @param className - The class name to look up (e.g., `'Document'`).
97
+ * @returns `true` if the class is tenant-scoped by either mechanism.
98
+ *
99
+ * @see getTenantScopedConfig
100
+ * @see registerTenantScopedClass
101
+ */
102
+ export declare function isTenantScopedClass(className: string): boolean;
103
+ /**
104
+ * Retrieve the resolved tenancy configuration for a class.
105
+ *
106
+ * Checks two sources in order, with the local registry taking precedence:
107
+ * 1. The local registry populated by `@TenantScoped()`.
108
+ * 2. The core `ObjectRegistry` populated by `@smrt({ tenantScoped: true })`.
109
+ *
110
+ * When found in the core registry, the raw config is normalised into a
111
+ * `TenantScopedConfig` with the same shape as locally registered classes.
112
+ *
113
+ * @param className - The class name to look up.
114
+ * @returns The `TenantScopedConfig` if the class is tenant-scoped, or
115
+ * `undefined` if it is not registered in either source.
116
+ *
117
+ * @see isTenantScopedClass
118
+ * @see getAllTenantScopedClasses
119
+ */
120
+ export declare function getTenantScopedConfig(className: string): TenantScopedConfig | undefined;
121
+ /**
122
+ * Return a snapshot of all classes registered via `@TenantScoped()`.
123
+ *
124
+ * Returns a new `Map` so mutations to the returned value do not affect the
125
+ * internal registry. Note that classes registered only through the core
126
+ * `ObjectRegistry` (`@smrt({ tenantScoped: true })`) are **not** included in
127
+ * this map.
128
+ *
129
+ * @returns A copy of the local tenant-scoped class registry, keyed by class name.
130
+ *
131
+ * @see isTenantScopedClass
132
+ * @see getTenantScopedConfig
133
+ */
134
+ export declare function getAllTenantScopedClasses(): Map<string, TenantScopedConfig>;
135
+ /**
136
+ * Remove all entries from the local tenant-scoped class registry.
137
+ *
138
+ * Intended for test teardown via `resetTenancy()`. Does not affect
139
+ * registrations held by the core `ObjectRegistry`.
140
+ *
141
+ * @see resetTenancy
142
+ * @see unregisterTenantScopedClass
143
+ */
144
+ export declare function clearTenantScopedRegistry(): void;
145
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH;;;;;;;;GAQG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;;;OAKG;IACH,IAAI,EAAE,UAAU,GAAG,UAAU,CAAC;IAE9B;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,UAAU,EAAE,OAAO,CAAC;IAEpB;;;OAGG;IACH,YAAY,EAAE,OAAO,CAAC;IAEtB;;;OAGG;IACH,qBAAqB,EAAE,OAAO,CAAC;CAChC;AAaD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,EACjB,MAAM,GAAE,OAAO,CAAC,kBAAkB,CAAM,GACvC,IAAI,CAKN;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAEnE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAO9D;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,MAAM,GAChB,kBAAkB,GAAG,SAAS,CAqBhC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,IAAI,GAAG,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAE3E;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,IAAI,IAAI,CAEhD"}
@@ -0,0 +1,65 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "generatedAt": "2026-06-23T01:11:11.521Z",
4
+ "packageName": "@happyvertical/smrt-tenancy",
5
+ "packageVersion": "0.30.0",
6
+ "sourceManifestPath": "dist/manifest.json",
7
+ "agentDocPath": "AGENTS.md",
8
+ "sourceHashes": {
9
+ "manifest": "34487a7449774d11873a68c8c58bfcde00949492655c695de325b5f110f3f83f",
10
+ "packageJson": "277085184246334ffbbe7841bffd1bbcfabe83d1159c3fa5d3faf1e66e77630f",
11
+ "agents": "6466580ac48829d3e51e940aaf42578919619e3a43fa7efbb741b744c80530c8"
12
+ },
13
+ "exports": [
14
+ ".",
15
+ "./adapters",
16
+ "./manifest",
17
+ "./manifest.json",
18
+ "./playground",
19
+ "./svelte",
20
+ "./testing",
21
+ "./ui"
22
+ ],
23
+ "dependencies": {
24
+ "@happyvertical/logger": "catalog:",
25
+ "@happyvertical/smrt-core": "workspace:*",
26
+ "@happyvertical/smrt-types": "workspace:*",
27
+ "@happyvertical/smrt-ui": "workspace:*",
28
+ "@happyvertical/sql": "catalog:",
29
+ "@happyvertical/utils": "catalog:",
30
+ "@happyvertical/smrt-vitest": "workspace:*",
31
+ "@sveltejs/package": "^2.5.7",
32
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
33
+ "@types/node": "25.0.9",
34
+ "svelte": "^5.46.4",
35
+ "svelte-check": "^4.3.5",
36
+ "typescript": "^5.9.3",
37
+ "vite": "^7.3.1",
38
+ "vitest": "^4.0.17"
39
+ },
40
+ "smrtDependencies": [
41
+ "@happyvertical/smrt-core",
42
+ "@happyvertical/smrt-types",
43
+ "@happyvertical/smrt-ui",
44
+ "@happyvertical/smrt-vitest"
45
+ ],
46
+ "sdkDependencies": [
47
+ "@happyvertical/logger",
48
+ "@happyvertical/sql",
49
+ "@happyvertical/utils"
50
+ ],
51
+ "tags": [],
52
+ "risks": [],
53
+ "objects": [],
54
+ "surfaces": [],
55
+ "prompts": [],
56
+ "relationshipsV2": {
57
+ "foreignKeyFields": 0,
58
+ "crossPackageRefFields": 0,
59
+ "junctionCollections": 0,
60
+ "hierarchicalObjects": 0,
61
+ "polymorphicAssociations": 0,
62
+ "uuidColumns": 0
63
+ },
64
+ "agentDoc": "# @happyvertical/smrt-tenancy\n\nMulti-tenancy via AsyncLocalStorage context propagation with automatic query filtering and tenant ID population.\n\n## Context Propagation\n\n```typescript\nimport { withTenant, getTenantId, withSystemContext } from '@happyvertical/smrt-tenancy';\n\nawait withTenant({ tenantId: 'tenant-123' }, async () => {\n // All SmrtCollection queries auto-filtered by tenantId\n // All creates auto-populate tenantId\n const docs = await collection.list({}); // WHERE tenant_id = 'tenant-123'\n});\n\nawait withSystemContext(async () => { /* bypasses all tenant checks */ });\n```\n\n**Critical distinction**: `withSystemContext()` sets a SYSTEM_CONTEXT_MARKER sentinel — different from \"no context\" (undefined). Interceptor can distinguish intentional bypass from missing context.\n\n## Interceptor System\n\nHooks into SmrtCollection via `GlobalInterceptors.register()` (priority 100, runs first):\n\n| Hook | Behavior |\n|------|----------|\n| `beforeList` | Injects `tenantId` into WHERE clause; validates existing filters match context |\n| `beforeGet` | Same — converts ID lookup to `{ id, tenantId }` |\n| `beforeSave` | Auto-populates tenantId if empty + `autoPopulate: true`; validates if already set |\n| `beforeDelete` | Validates instance.tenantId matches context |\n| `beforeQuery` | Enforces raw SQL policy on tenant-scoped classes (`throw`/`warn`/`allow`) |\n| `afterSave` | Emits `directory.<class>.created`/`updated` via `dispatchBus` for configured `directoryClasses` |\n| `afterDelete` | Emits `directory.<class>.deleted` via `dispatchBus` for configured `directoryClasses` |\n\nMismatches throw `TenantIsolationError`. Missing required context throws `TenantContextError`.\n\n## Registration — Two Patterns\n\n```typescript\n// Pattern 1: Tenancy decorator\n@TenantScoped({ mode: 'optional' })\nclass Doc extends SmrtObject { @tenantId({ nullable: true }) tenantId: string | null = null; }\n\n// Pattern 2: Core decorator (tenancy package reads this too)\n@smrt({ tenantScoped: { mode: 'optional' } })\nclass Doc extends SmrtObject { tenantId: string | null = null; }\n```\n\nModes: `'required'` (default — throws without context) or `'optional'` (passes through if no context).\n\n## Adapters\n\n- **Express**: `createExpressMiddleware()` — uses `enterTenantContext()` (not withTenant, because middleware returns before handlers run)\n- **SvelteKit**: `createSvelteKitHandle()` — stores context in `event.locals`\n- **CLI**: `createCliContext()` — `run()`, `runWithTenant()`, `runAsSystem()`, `runAsSuperAdmin()`\n\n## Super Admin Bypass\n\n`withSuperAdminBypass()` keeps tenant context but disables auto-filtering. Different from `withSystemContext()` which removes context entirely.\n\n## Gotchas\n\n- **Context lost in callbacks**: `setTimeout(() => getTenantId(), 100)` → undefined. Fix: `TenantContext.bind(fn)`\n- **Nested contexts override**: inner `withTenant()` overrides outer; restores on exit\n- **Auto-populate only if empty**: if tenantId already set, interceptor validates (not overwrites)\n- **Isolation checked at query time**: `list({ where: { tenantId: 'other' } })` throws immediately\n- **Testing**: `resetTenancy()` + `setupTestTenancy()` in beforeEach; `testTenantIsolation()` helper\n\n## Known exceptions to monorepo standards\n\n- **`serializeInstance()` in `src/interceptor.ts` calls `instance.toJSON()` directly** (standards.md §7 forbids this in favor of `transformJSON()`). The interceptor must serialize arbitrary instances handed to it — including workspace stubs and plain-object test doubles whose classes may not extend `SmrtObject` and therefore have no `transformJSON()` hook. The call is duck-typed and falls back to manual key iteration when `toJSON` is absent. See the inline comment at the call site for the full rationale.\n"
65
+ }
@@ -0,0 +1,272 @@
1
+ <script lang="ts">
2
+ /**
3
+ * TenantCard - Tenant information and management
4
+ * refactored for Material 3
5
+ */
6
+
7
+ import type { Tenant } from '@happyvertical/smrt-types';
8
+ import { Icon, ripple } from '@happyvertical/smrt-ui';
9
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
10
+ import { M } from '../i18n.js';
11
+
12
+ export interface Props {
13
+ tenant: Tenant;
14
+ memberCount?: number;
15
+ onclick?: () => void;
16
+ selected?: boolean;
17
+ actions?: boolean;
18
+ onedit?: () => void;
19
+ ondelete?: () => void;
20
+ }
21
+
22
+ const {
23
+ tenant,
24
+ memberCount,
25
+ onclick,
26
+ selected = false,
27
+ actions = false,
28
+ onedit,
29
+ ondelete,
30
+ }: Props = $props();
31
+
32
+ const { t } = useI18n();
33
+
34
+ const statusClass = $derived.by(() => {
35
+ switch (tenant.status) {
36
+ case 'active':
37
+ return 'status-active';
38
+ case 'suspended':
39
+ return 'status-error';
40
+ case 'archived':
41
+ return 'status-disabled';
42
+ default:
43
+ return '';
44
+ }
45
+ });
46
+
47
+ function getInitials(name: string): string {
48
+ return name
49
+ .split(' ')
50
+ .map((part) => part[0])
51
+ .join('')
52
+ .toUpperCase()
53
+ .slice(0, 2);
54
+ }
55
+
56
+ function getColor(name: string): string {
57
+ const colors = [
58
+ '#3b82f6',
59
+ '#8b5cf6',
60
+ '#ec4899',
61
+ '#f97316',
62
+ '#22c55e',
63
+ '#06b6d4',
64
+ '#6366f1',
65
+ '#14b8a6',
66
+ ];
67
+ let hash = 0;
68
+ for (let i = 0; i < name.length; i++) {
69
+ hash = name.charCodeAt(i) + ((hash << 5) - hash);
70
+ }
71
+ return colors[Math.abs(hash) % colors.length];
72
+ }
73
+ </script>
74
+
75
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
76
+ <div
77
+ class="tenant-card"
78
+ class:clickable={!!onclick}
79
+ class:selected
80
+ role={onclick ? 'button' : undefined}
81
+ tabindex={onclick ? 0 : undefined}
82
+ onclick={onclick}
83
+ onkeydown={(event: KeyboardEvent) => event.key === 'Enter' && onclick?.()}
84
+ use:ripple
85
+ >
86
+ <div class="avatar" style:background-color={getColor(tenant.name ?? '')}>
87
+ {getInitials(tenant.name ?? 'T')}
88
+ </div>
89
+
90
+ <div class="info">
91
+ <div class="header">
92
+ <span class="name">{tenant.name}</span>
93
+ <span class="status {statusClass}">{tenant.status}</span>
94
+ </div>
95
+
96
+ {#if tenant.slug}
97
+ <div class="slug">{tenant.slug}</div>
98
+ {/if}
99
+
100
+ {#if memberCount !== undefined}
101
+ <div class="meta">
102
+ <span class="members">{memberCount} member{memberCount === 1 ? '' : 's'}</span>
103
+ </div>
104
+ {/if}
105
+ </div>
106
+
107
+ {#if actions && (onedit || ondelete)}
108
+ <div class="actions">
109
+ {#if onedit}
110
+ <button
111
+ type="button"
112
+ class="action-btn"
113
+ onclick={(event: MouseEvent) => { event.stopPropagation(); onedit?.(); }}
114
+ use:ripple
115
+ aria-label={t(M['tenancy.tenant_card.edit'])}
116
+ >
117
+ <Icon name="menu" size={18} />
118
+ </button>
119
+ {/if}
120
+ {#if ondelete}
121
+ <button
122
+ type="button"
123
+ class="action-btn danger"
124
+ onclick={(event: MouseEvent) => { event.stopPropagation(); ondelete?.(); }}
125
+ use:ripple
126
+ aria-label={t(M['tenancy.tenant_card.delete'])}
127
+ >
128
+ <Icon name="close" size={18} />
129
+ </button>
130
+ {/if}
131
+ </div>
132
+ {/if}
133
+ </div>
134
+
135
+ <style>
136
+ .tenant-card {
137
+ display: flex;
138
+ align-items: center;
139
+ gap: var(--smrt-spacing-4, 16px);
140
+ padding: var(--smrt-spacing-4, 16px);
141
+ background-color: var(--smrt-color-surface-container-low);
142
+ border-radius: var(--smrt-radius-lg, 12px);
143
+ color: var(--smrt-color-on-surface);
144
+ transition: all 200ms cubic-bezier(0.2, 0, 0, 1);
145
+ position: relative;
146
+ overflow: hidden;
147
+ box-shadow: var(--smrt-elevation-1);
148
+ }
149
+
150
+ .tenant-card.clickable {
151
+ cursor: pointer;
152
+ }
153
+
154
+ .tenant-card.clickable:hover {
155
+ background-color: var(--smrt-color-surface-container-high);
156
+ box-shadow: var(--smrt-elevation-2);
157
+ }
158
+
159
+ .tenant-card.selected {
160
+ background-color: var(--smrt-color-secondary-container);
161
+ color: var(--smrt-color-on-secondary-container);
162
+ }
163
+
164
+ .avatar {
165
+ display: flex;
166
+ align-items: center;
167
+ justify-content: center;
168
+ width: 48px;
169
+ height: 48px;
170
+ border-radius: var(--smrt-radius-lg, 12px);
171
+ color: white;
172
+ font: var(--smrt-typography-title-medium-font);
173
+ font-weight: var(--smrt-typography-weight-semibold, 600);
174
+ flex-shrink: 0;
175
+ }
176
+
177
+ .info {
178
+ flex: 1;
179
+ min-width: 0;
180
+ }
181
+
182
+ .header {
183
+ display: flex;
184
+ align-items: center;
185
+ gap: var(--smrt-spacing-2, 8px);
186
+ }
187
+
188
+ .name {
189
+ font: var(--smrt-typography-title-small-font);
190
+ font-weight: var(--smrt-typography-weight-semibold, 600);
191
+ white-space: nowrap;
192
+ overflow: hidden;
193
+ text-overflow: ellipsis;
194
+ }
195
+
196
+ .status {
197
+ font: var(--smrt-typography-label-small-font);
198
+ padding: 0 var(--smrt-spacing-2, 8px);
199
+ height: 18px;
200
+ display: inline-flex;
201
+ align-items: center;
202
+ border-radius: var(--smrt-radius-md, 8px);
203
+ text-transform: uppercase;
204
+ font-weight: var(--smrt-typography-weight-semibold, 600);
205
+ flex-shrink: 0;
206
+ }
207
+
208
+ .status-active {
209
+ background-color: var(--smrt-color-primary-container);
210
+ color: var(--smrt-color-on-primary-container);
211
+ }
212
+
213
+ .status-error {
214
+ background-color: var(--smrt-color-error-container);
215
+ color: var(--smrt-color-on-error-container);
216
+ }
217
+
218
+ .status-disabled {
219
+ background-color: var(--smrt-color-surface-variant);
220
+ color: var(--smrt-color-on-surface-variant);
221
+ }
222
+
223
+ .slug {
224
+ font: var(--smrt-typography-body-small-font);
225
+ color: var(--smrt-color-on-surface-variant);
226
+ margin-top: var(--smrt-spacing-1, 4px);
227
+ }
228
+
229
+ .selected .slug {
230
+ color: var(--smrt-color-on-secondary-container);
231
+ opacity: 0.8;
232
+ }
233
+
234
+ .meta {
235
+ margin-top: var(--smrt-spacing-1, 4px);
236
+ }
237
+
238
+ .members {
239
+ font: var(--smrt-typography-label-small-font);
240
+ color: var(--smrt-color-on-surface-variant);
241
+ }
242
+
243
+ .actions {
244
+ display: flex;
245
+ gap: var(--smrt-spacing-1, 4px);
246
+ flex-shrink: 0;
247
+ }
248
+
249
+ .action-btn {
250
+ display: flex;
251
+ align-items: center;
252
+ justify-content: center;
253
+ width: 32px;
254
+ height: 32px;
255
+ background: transparent;
256
+ border: none;
257
+ border-radius: var(--smrt-radius-full, 9999px);
258
+ cursor: pointer;
259
+ color: var(--smrt-color-on-surface-variant);
260
+ position: relative;
261
+ overflow: hidden;
262
+ }
263
+
264
+ .action-btn:hover {
265
+ background-color: var(--smrt-color-surface-container-highest);
266
+ color: var(--smrt-color-on-surface);
267
+ }
268
+
269
+ .action-btn.danger:hover {
270
+ color: var(--smrt-color-error);
271
+ }
272
+ </style>
@@ -0,0 +1,18 @@
1
+ /**
2
+ * TenantCard - Tenant information and management
3
+ * refactored for Material 3
4
+ */
5
+ import type { Tenant } from '@happyvertical/smrt-types';
6
+ export interface Props {
7
+ tenant: Tenant;
8
+ memberCount?: number;
9
+ onclick?: () => void;
10
+ selected?: boolean;
11
+ actions?: boolean;
12
+ onedit?: () => void;
13
+ ondelete?: () => void;
14
+ }
15
+ declare const TenantCard: import("svelte").Component<Props, {}, "">;
16
+ type TenantCard = ReturnType<typeof TenantCard>;
17
+ export default TenantCard;
18
+ //# sourceMappingURL=TenantCard.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TenantCard.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/TenantCard.svelte.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAC;AAMxD,MAAM,WAAW,KAAK;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;CACvB;AA0GD,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
@@ -0,0 +1,68 @@
1
+ <script lang="ts">
2
+ import type { Membership, Tenant } from '@happyvertical/smrt-types';
3
+
4
+ export interface Props {
5
+ memberships: Membership[];
6
+ tenants: Map<string, Tenant>;
7
+ currentTenantId: string;
8
+ onchange?: (tenantId: string) => void;
9
+ }
10
+
11
+ const { memberships, tenants, currentTenantId, onchange }: Props = $props();
12
+
13
+ const currentTenant = $derived(tenants.get(currentTenantId));
14
+
15
+ function handleChange(event: Event) {
16
+ const select = event.target as HTMLSelectElement;
17
+ onchange?.(select.value);
18
+ }
19
+ </script>
20
+
21
+ <div class="tenant-switcher">
22
+ {#if memberships.length <= 1}
23
+ <span class="tenant-name">{currentTenant?.name ?? 'Unknown'}</span>
24
+ {:else}
25
+ <select value={currentTenantId} onchange={handleChange} class="tenant-select">
26
+ {#each memberships as membership}
27
+ {#if membership.tenantId}
28
+ {@const tenant = tenants.get(membership.tenantId)}
29
+ <option value={membership.tenantId}>
30
+ {tenant?.name ?? 'Unknown'}
31
+ </option>
32
+ {/if}
33
+ {/each}
34
+ </select>
35
+ {/if}
36
+ </div>
37
+
38
+ <style>
39
+ .tenant-switcher {
40
+ display: inline-flex;
41
+ align-items: center;
42
+ }
43
+
44
+ .tenant-name {
45
+ font-weight: var(--smrt-typography-weight-medium, 500);
46
+ }
47
+
48
+ .tenant-select {
49
+ padding: var(--smrt-spacing-sm, 0.5rem) var(--smrt-spacing-md, 1rem);
50
+ border: 1px solid var(--smrt-color-outline-variant, #c4c6cf);
51
+ border-radius: var(--smrt-radius-medium, 0.5rem);
52
+ background: var(--smrt-color-surface, white);
53
+ font: var(--smrt-typography-body-medium-font, 0.875rem / 1.25 sans-serif);
54
+ cursor: pointer;
55
+ transition: border-color var(--smrt-duration-short2, 150ms) var(--smrt-easing-standard, ease);
56
+ }
57
+
58
+ .tenant-select:focus {
59
+ outline: 2px solid var(--smrt-color-primary, #005ac1);
60
+ outline-offset: 2px;
61
+ }
62
+
63
+ @media (prefers-reduced-motion: reduce) {
64
+ .tenant-select {
65
+ transition: none;
66
+ }
67
+ }
68
+ </style>
@@ -0,0 +1,11 @@
1
+ import type { Membership, Tenant } from '@happyvertical/smrt-types';
2
+ export interface Props {
3
+ memberships: Membership[];
4
+ tenants: Map<string, Tenant>;
5
+ currentTenantId: string;
6
+ onchange?: (tenantId: string) => void;
7
+ }
8
+ declare const TenantSwitcher: import("svelte").Component<Props, {}, "">;
9
+ type TenantSwitcher = ReturnType<typeof TenantSwitcher>;
10
+ export default TenantSwitcher;
11
+ //# sourceMappingURL=TenantSwitcher.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TenantSwitcher.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/TenantSwitcher.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAC;AAGpE,MAAM,WAAW,KAAK;IACpB,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC;AAoCD,QAAA,MAAM,cAAc,2CAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
@@ -0,0 +1,5 @@
1
+ export declare const M: {
2
+ readonly 'tenancy.tenant_card.edit': "tenancy.tenant_card.edit";
3
+ readonly 'tenancy.tenant_card.delete': "tenancy.tenant_card.delete";
4
+ };
5
+ //# sourceMappingURL=i18n.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../../src/svelte/i18n.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,CAAC;;;CAGZ,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * smrt-tenancy UI message catalog (S13 #1418).
3
+ * Keys: `tenancy.<component>.<descriptor>`.
4
+ */
5
+ import { defineMessages } from '@happyvertical/smrt-ui/i18n';
6
+ export const M = defineMessages({
7
+ 'tenancy.tenant_card.edit': 'Edit',
8
+ 'tenancy.tenant_card.delete': 'Delete',
9
+ });
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Tenancy Module Svelte Components
3
+ *
4
+ * Optional Svelte UI components for tenant management.
5
+ * Auto-registers components with ModuleUIRegistry on import.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+ import type { ComponentProps } from 'svelte';
10
+ import TenantCard from './components/TenantCard.svelte';
11
+ import TenantSwitcher from './components/TenantSwitcher.svelte';
12
+ export { TenantCard, TenantSwitcher };
13
+ export type TenantCardProps = ComponentProps<typeof TenantCard>;
14
+ export type TenantSwitcherProps = ComponentProps<typeof TenantSwitcher>;
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/svelte/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAI7C,OAAO,UAAU,MAAM,gCAAgC,CAAC;AACxD,OAAO,cAAc,MAAM,oCAAoC,CAAC;AAGhE,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,CAAC;AAGtC,MAAM,MAAM,eAAe,GAAG,cAAc,CAAC,OAAO,UAAU,CAAC,CAAC;AAChE,MAAM,MAAM,mBAAmB,GAAG,cAAc,CAAC,OAAO,cAAc,CAAC,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Tenancy Module Svelte Components
3
+ *
4
+ * Optional Svelte UI components for tenant management.
5
+ * Auto-registers components with ModuleUIRegistry on import.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+ import { ModuleUIRegistry } from '@happyvertical/smrt-ui/registry';
10
+ import { TENANCY_MODULE_META } from '../ui.js';
11
+ // Import components
12
+ import TenantCard from './components/TenantCard.svelte';
13
+ import TenantSwitcher from './components/TenantSwitcher.svelte';
14
+ // Export components
15
+ export { TenantCard, TenantSwitcher };
16
+ // Auto-register with ModuleUIRegistry
17
+ ModuleUIRegistry.registerModule(TENANCY_MODULE_META);
18
+ ModuleUIRegistry.register('@happyvertical/smrt-tenancy', 'tenant-card', TenantCard);
19
+ ModuleUIRegistry.register('@happyvertical/smrt-tenancy', 'tenant-switcher', TenantSwitcher);