@csntgao/uni-base 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/.prettierignore +4 -0
  2. package/.prettierrc.json +6 -0
  3. package/AGENTS.md +519 -0
  4. package/CLAUDE.md +512 -0
  5. package/README.md +734 -0
  6. package/docs//346/225/260/345/255/227/345/221/230/345/267/245/345/256/214/346/225/264/346/212/275/345/261/211/346/216/245/345/205/245/350/257/264/346/230/216.md +110 -0
  7. package/docs//347/273/204/347/273/207/345/221/230/345/267/245/345/215/225/351/200/211/347/273/204/344/273/266/346/216/245/345/205/245/350/257/264/346/230/216.md +79 -0
  8. package/docs//347/273/204/347/273/207/345/221/230/345/267/245/345/244/232/351/200/211/347/273/204/344/273/266/346/216/245/345/205/245/350/257/264/346/230/216.md +93 -0
  9. package/docs//350/264/246/346/210/267/344/270/216/347/231/273/345/275/225/347/273/204/344/273/266/345/212/250/346/200/201/346/216/245/345/205/245/350/257/264/346/230/216.md +201 -0
  10. package/eslint.config.js +30 -0
  11. package/examples/vanilla/index.html +33 -0
  12. package/examples/vanilla/main.js +247 -0
  13. package/examples/vanilla/package.json +15 -0
  14. package/examples/vanilla/public/runtime-config.js +10 -0
  15. package/examples/vanilla/style.css +99 -0
  16. package/package.json +31 -0
  17. package/packages/core/package.json +30 -0
  18. package/packages/core/src/client.ts +1132 -0
  19. package/packages/core/src/customer-support-client.ts +825 -0
  20. package/packages/core/src/customer-support-types.ts +123 -0
  21. package/packages/core/src/index.ts +79 -0
  22. package/packages/core/src/notification-center-client.ts +462 -0
  23. package/packages/core/src/notification-center-types.ts +154 -0
  24. package/packages/core/src/protocol.ts +237 -0
  25. package/packages/core/src/types.ts +170 -0
  26. package/packages/core/tests/client.test.ts +760 -0
  27. package/packages/core/tests/customer-support-client.test.ts +576 -0
  28. package/packages/core/tests/fake-websocket.ts +55 -0
  29. package/packages/core/tests/notification-center-client.test.ts +467 -0
  30. package/packages/core/tests/protocol.test.ts +124 -0
  31. package/packages/core/tsconfig.json +7 -0
  32. package/packages/core/tsup.config.ts +11 -0
  33. package/packages/web-components/package.json +35 -0
  34. package/packages/web-components/scripts/verify-runtime-build.mjs +111 -0
  35. package/packages/web-components/src/account-context.ts +1540 -0
  36. package/packages/web-components/src/application-switcher-transport.ts +243 -0
  37. package/packages/web-components/src/application-switcher.ts +1103 -0
  38. package/packages/web-components/src/chat.ts +1754 -0
  39. package/packages/web-components/src/customer-support-chat.ts +862 -0
  40. package/packages/web-components/src/customer-support-transport.ts +269 -0
  41. package/packages/web-components/src/index.ts +256 -0
  42. package/packages/web-components/src/notification-center.ts +1066 -0
  43. package/packages/web-components/src/notification-popover.ts +615 -0
  44. package/packages/web-components/src/organization-multi-employee-selector.ts +1564 -0
  45. package/packages/web-components/src/organization-onboarding-transport.ts +54 -0
  46. package/packages/web-components/src/organization-onboarding.ts +1216 -0
  47. package/packages/web-components/src/organization-selector.ts +534 -0
  48. package/packages/web-components/src/organization-single-employee-selector.ts +114 -0
  49. package/packages/web-components/src/prototype-icons.ts +242 -0
  50. package/packages/web-components/src/safe-image-url.ts +29 -0
  51. package/packages/web-components/src/safe-markdown.ts +192 -0
  52. package/packages/web-components/src/uniplat-base-account-context-transport.ts +625 -0
  53. package/packages/web-components/src/uniplat-base-application-switcher-transport.ts +418 -0
  54. package/packages/web-components/src/uniplat-base-application-ticket.ts +254 -0
  55. package/packages/web-components/src/uniplat-base-identity-ticket.ts +202 -0
  56. package/packages/web-components/src/uniplat-base-notification-center-transport.ts +378 -0
  57. package/packages/web-components/src/uniplat-base-organization-multi-employee-selector-transport.ts +394 -0
  58. package/packages/web-components/src/uniplat-base-organization-onboarding-transport.ts +183 -0
  59. package/packages/web-components/src/uniplat-base-organization-single-employee-selector-transport.ts +14 -0
  60. package/packages/web-components/src/user-login-transport.ts +563 -0
  61. package/packages/web-components/src/user-login.ts +2027 -0
  62. package/packages/web-components/src/user-menu.ts +880 -0
  63. package/packages/web-components/tests/account-context.test.ts +798 -0
  64. package/packages/web-components/tests/application-switcher.test.ts +562 -0
  65. package/packages/web-components/tests/chat.test.ts +1130 -0
  66. package/packages/web-components/tests/customer-support-chat.test.ts +428 -0
  67. package/packages/web-components/tests/customer-support-transport.test.ts +79 -0
  68. package/packages/web-components/tests/notification-center-transport.test.ts +271 -0
  69. package/packages/web-components/tests/notification-center.test.ts +429 -0
  70. package/packages/web-components/tests/notification-popover.test.ts +283 -0
  71. package/packages/web-components/tests/organization-multi-employee-selector-transport.test.ts +332 -0
  72. package/packages/web-components/tests/organization-multi-employee-selector.test.ts +225 -0
  73. package/packages/web-components/tests/organization-onboarding.test.ts +260 -0
  74. package/packages/web-components/tests/organization-selector.test.ts +127 -0
  75. package/packages/web-components/tests/organization-single-employee-selector.test.ts +145 -0
  76. package/packages/web-components/tests/uniplat-base-account-context-transport.test.ts +496 -0
  77. package/packages/web-components/tests/uniplat-base-application-switcher-transport.test.ts +598 -0
  78. package/packages/web-components/tests/uniplat-base-application-ticket.test.ts +233 -0
  79. package/packages/web-components/tests/uniplat-base-identity-ticket.test.ts +226 -0
  80. package/packages/web-components/tests/uniplat-base-organization-onboarding-transport.test.ts +227 -0
  81. package/packages/web-components/tests/user-login-transport.test.ts +443 -0
  82. package/packages/web-components/tests/user-login.test.ts +626 -0
  83. package/packages/web-components/tests/user-menu.test.ts +271 -0
  84. package/packages/web-components/tsconfig.json +8 -0
  85. package/packages/web-components/tsup.config.ts +12 -0
  86. package/packages/web-components/tsup.runtime.config.ts +37 -0
  87. package/pnpm-workspace.yaml +6 -0
  88. package/tsconfig.base.json +21 -0
  89. package/vitest.config.ts +20 -0
  90. package//344/272/244/346/216/245/346/226/207/346/241/243.md +254 -0
@@ -0,0 +1,534 @@
1
+ import { LitElement, css, html, nothing, type TemplateResult } from 'lit'
2
+
3
+ export type OrganizationSelectorType = 'enterprise' | 'team'
4
+ export type EnterpriseVerificationStatus =
5
+ 'unverified' | 'pending' | 'rejected' | 'verified' | 'expired'
6
+
7
+ export interface OrganizationSelectorEntry {
8
+ id: string
9
+ name: string
10
+ type: OrganizationSelectorType
11
+ verificationStatus?: EnterpriseVerificationStatus
12
+ role?: string
13
+ memberCount?: number
14
+ lastEnteredLabel?: string
15
+ }
16
+
17
+ export interface OrganizationSelectorConfig {
18
+ organizations: readonly OrganizationSelectorEntry[]
19
+ selectedOrganizationId?: string
20
+ rememberSelection?: boolean
21
+ }
22
+
23
+ export interface OrganizationSelectorEventMap {
24
+ 'organization-enter-requested': CustomEvent<{
25
+ organizationId: string
26
+ rememberSelection: boolean
27
+ }>
28
+ 'organization-create-requested': CustomEvent<Record<string, never>>
29
+ }
30
+
31
+ interface ResolvedOrganizationSelectorEntry {
32
+ id: string
33
+ name: string
34
+ type: OrganizationSelectorType
35
+ verificationStatus: EnterpriseVerificationStatus | null
36
+ role: string
37
+ memberCount: number | null
38
+ lastEnteredLabel: string
39
+ }
40
+
41
+ export class UniplatOrganizationSelector extends LitElement {
42
+ static override properties = {
43
+ organizations: { state: true },
44
+ selectedOrganizationId: { state: true },
45
+ rememberSelection: { state: true },
46
+ }
47
+
48
+ static override styles = css`
49
+ :host {
50
+ --uniplat-organization-selector-width: 400px;
51
+ --uniplat-organization-selector-primary: #2e6bf5;
52
+ --uniplat-organization-selector-text: #182634;
53
+ --uniplat-organization-selector-muted: #7a8794;
54
+ --uniplat-organization-selector-border: #e4ebf3;
55
+ --uniplat-organization-selector-font-family:
56
+ Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
57
+ display: block;
58
+ width: min(var(--uniplat-organization-selector-width), 100%);
59
+ color: var(--uniplat-organization-selector-text);
60
+ font-family: var(--uniplat-organization-selector-font-family);
61
+ line-height: 1.5;
62
+ }
63
+
64
+ * {
65
+ box-sizing: border-box;
66
+ }
67
+
68
+ button,
69
+ input {
70
+ font: inherit;
71
+ }
72
+
73
+ button {
74
+ cursor: pointer;
75
+ }
76
+
77
+ .card {
78
+ width: 100%;
79
+ padding: 24px;
80
+ overflow: hidden;
81
+ border: 1px solid var(--uniplat-organization-selector-border);
82
+ border-radius: 16px;
83
+ background: #ffffff;
84
+ box-shadow: 0 8px 24px rgba(20, 40, 80, 0.08);
85
+ }
86
+
87
+ h2,
88
+ p {
89
+ margin: 0;
90
+ }
91
+
92
+ h2 {
93
+ font-size: 20px;
94
+ font-weight: 800;
95
+ line-height: 1.35;
96
+ }
97
+
98
+ .description {
99
+ margin-top: 7px;
100
+ color: var(--uniplat-organization-selector-muted);
101
+ font-size: 13px;
102
+ }
103
+
104
+ .list {
105
+ display: grid;
106
+ gap: 9px;
107
+ max-height: 340px;
108
+ margin-top: 18px;
109
+ overflow-y: auto;
110
+ scrollbar-gutter: stable;
111
+ }
112
+
113
+ .organization {
114
+ display: grid;
115
+ grid-template-columns: 36px minmax(0, 1fr) 18px;
116
+ align-items: center;
117
+ gap: 10px;
118
+ width: 100%;
119
+ min-height: 62px;
120
+ padding: 10px 11px;
121
+ border: 1px solid #edf1f6;
122
+ border-radius: 10px;
123
+ color: inherit;
124
+ background: #ffffff;
125
+ text-align: left;
126
+ }
127
+
128
+ .organization:hover {
129
+ background: #fafcff;
130
+ }
131
+
132
+ .organization.selected {
133
+ border-color: var(--uniplat-organization-selector-primary);
134
+ background: #f7f9ff;
135
+ }
136
+
137
+ .organization-icon {
138
+ display: grid;
139
+ place-items: center;
140
+ width: 36px;
141
+ height: 36px;
142
+ border-radius: 9px;
143
+ color: var(--uniplat-organization-selector-primary);
144
+ background: #e6eefe;
145
+ }
146
+
147
+ .organization.team .organization-icon {
148
+ color: #0ca678;
149
+ background: #e3f7ef;
150
+ }
151
+
152
+ .copy {
153
+ min-width: 0;
154
+ }
155
+
156
+ .name-line {
157
+ display: flex;
158
+ align-items: center;
159
+ gap: 6px;
160
+ min-width: 0;
161
+ }
162
+
163
+ .name {
164
+ overflow: hidden;
165
+ font-size: 13.5px;
166
+ font-weight: 750;
167
+ text-overflow: ellipsis;
168
+ white-space: nowrap;
169
+ }
170
+
171
+ .badge {
172
+ flex: none;
173
+ padding: 1px 6px;
174
+ border-radius: 4px;
175
+ color: var(--uniplat-organization-selector-primary);
176
+ background: #e6eefe;
177
+ font-size: 10.5px;
178
+ font-weight: 700;
179
+ }
180
+
181
+ .badge.team {
182
+ color: #0ca678;
183
+ background: #e3f7ef;
184
+ }
185
+
186
+ .badge.unverified,
187
+ .badge.rejected,
188
+ .badge.expired {
189
+ color: #e8590c;
190
+ background: #fff4e2;
191
+ }
192
+
193
+ .badge.pending {
194
+ color: #1971c2;
195
+ background: #e2effb;
196
+ }
197
+
198
+ .meta {
199
+ display: block;
200
+ margin-top: 2px;
201
+ overflow: hidden;
202
+ color: #8b98a6;
203
+ font-size: 11.5px;
204
+ text-overflow: ellipsis;
205
+ white-space: nowrap;
206
+ }
207
+
208
+ .radio {
209
+ display: grid;
210
+ place-items: center;
211
+ width: 18px;
212
+ height: 18px;
213
+ border: 1.5px solid #c7d1df;
214
+ border-radius: 50%;
215
+ color: transparent;
216
+ }
217
+
218
+ .selected .radio {
219
+ border-color: var(--uniplat-organization-selector-primary);
220
+ color: #ffffff;
221
+ background: var(--uniplat-organization-selector-primary);
222
+ }
223
+
224
+ .footer-row {
225
+ display: flex;
226
+ align-items: center;
227
+ justify-content: space-between;
228
+ gap: 12px;
229
+ margin-top: 15px;
230
+ }
231
+
232
+ .remember {
233
+ display: inline-flex;
234
+ align-items: center;
235
+ gap: 7px;
236
+ color: #7a8794;
237
+ font-size: 12px;
238
+ cursor: pointer;
239
+ }
240
+
241
+ .remember input {
242
+ width: 15px;
243
+ height: 15px;
244
+ margin: 0;
245
+ accent-color: var(--uniplat-organization-selector-primary);
246
+ }
247
+
248
+ .create {
249
+ padding: 4px 0;
250
+ border: 0;
251
+ color: var(--uniplat-organization-selector-primary);
252
+ background: transparent;
253
+ font-size: 12.5px;
254
+ font-weight: 650;
255
+ }
256
+
257
+ .enter {
258
+ display: flex;
259
+ align-items: center;
260
+ justify-content: center;
261
+ width: 100%;
262
+ min-height: 46px;
263
+ margin-top: 13px;
264
+ border: 0;
265
+ border-radius: 10px;
266
+ color: #ffffff;
267
+ background: var(--uniplat-organization-selector-primary);
268
+ font-size: 14px;
269
+ font-weight: 700;
270
+ }
271
+
272
+ .enter:disabled {
273
+ cursor: not-allowed;
274
+ background: #a9bff6;
275
+ }
276
+
277
+ .empty {
278
+ display: grid;
279
+ min-height: 120px;
280
+ place-items: center;
281
+ margin-top: 18px;
282
+ border: 1px dashed #dce5f0;
283
+ border-radius: 10px;
284
+ color: #8b98a6;
285
+ font-size: 13px;
286
+ }
287
+
288
+ svg {
289
+ display: block;
290
+ width: 20px;
291
+ height: 20px;
292
+ fill: none;
293
+ stroke: currentColor;
294
+ stroke-linecap: round;
295
+ stroke-linejoin: round;
296
+ stroke-width: 1.8;
297
+ }
298
+ `
299
+
300
+ declare private organizations: readonly ResolvedOrganizationSelectorEntry[]
301
+ declare private selectedOrganizationId: string
302
+ declare private rememberSelection: boolean
303
+
304
+ private configured = false
305
+
306
+ constructor() {
307
+ super()
308
+ this.organizations = []
309
+ this.selectedOrganizationId = ''
310
+ this.rememberSelection = true
311
+ }
312
+
313
+ configure(config: OrganizationSelectorConfig): void {
314
+ if (!config || !Array.isArray(config.organizations)) {
315
+ throw new TypeError('organization-selector requires an organizations array')
316
+ }
317
+ const organizations = normalizeOrganizations(config.organizations)
318
+ const requestedSelection = normalizeOptionalText(config.selectedOrganizationId, 128)
319
+ this.organizations = organizations
320
+ this.selectedOrganizationId = organizations.some(({ id }) => id === requestedSelection)
321
+ ? requestedSelection
322
+ : (organizations[0]?.id ?? '')
323
+ this.rememberSelection = config.rememberSelection ?? true
324
+ this.configured = true
325
+ this.requestUpdate()
326
+ }
327
+
328
+ override render() {
329
+ if (!this.configured) return nothing
330
+ return html`
331
+ <section class="card" part="shell" aria-label="选择团队或企业">
332
+ <h2 part="title">选择要进入的团队 / 企业</h2>
333
+ <p class="description" part="description">
334
+ 你归属 ${this.organizations.length} 个团队 / 企业,可随时在导航中切换
335
+ </p>
336
+ ${
337
+ this.organizations.length
338
+ ? html`<div class="list" part="organization-list">
339
+ ${this.organizations.map((organization) => this.renderOrganization(organization))}
340
+ </div>`
341
+ : html`<div class="empty" part="empty">暂无可进入的团队或企业</div>`
342
+ }
343
+ <div class="footer-row">
344
+ <label class="remember" part="remember-selection">
345
+ <input
346
+ type="checkbox"
347
+ .checked=${this.rememberSelection}
348
+ @change=${(event: Event) => {
349
+ this.rememberSelection = (event.currentTarget as HTMLInputElement).checked
350
+ }}
351
+ />
352
+ 记住选择,下次直接进入
353
+ </label>
354
+ <button class="create" part="create-action" type="button" @click=${this.requestCreate}>
355
+ + 创建
356
+ </button>
357
+ </div>
358
+ <button
359
+ class="enter"
360
+ part="enter-action"
361
+ type="button"
362
+ ?disabled=${!this.selectedOrganizationId}
363
+ @click=${this.requestEnter}
364
+ >
365
+ 进入
366
+ </button>
367
+ </section>
368
+ `
369
+ }
370
+
371
+ private renderOrganization(organization: ResolvedOrganizationSelectorEntry) {
372
+ const selected = organization.id === this.selectedOrganizationId
373
+ const badge = organizationBadge(organization)
374
+ return html`
375
+ <button
376
+ class="organization ${organization.type === 'team' ? 'team' : ''} ${
377
+ selected ? 'selected' : ''
378
+ }"
379
+ part="organization"
380
+ type="button"
381
+ aria-pressed=${String(selected)}
382
+ @click=${() => {
383
+ this.selectedOrganizationId = organization.id
384
+ }}
385
+ >
386
+ <span class="organization-icon">${icon(organization.type)}</span>
387
+ <span class="copy">
388
+ <span class="name-line">
389
+ <span class="name">${organization.name}</span>
390
+ ${
391
+ badge
392
+ ? html`<span class="badge ${badge.className}" part="organization-badge">
393
+ ${badge.label}
394
+ </span>`
395
+ : nothing
396
+ }
397
+ </span>
398
+ <span class="meta">${organizationMeta(organization)}</span>
399
+ </span>
400
+ <span class="radio">${icon('check')}</span>
401
+ </button>
402
+ `
403
+ }
404
+
405
+ private readonly requestCreate = () => {
406
+ this.dispatchEvent(
407
+ new CustomEvent('organization-create-requested', {
408
+ detail: {},
409
+ bubbles: true,
410
+ composed: true,
411
+ }),
412
+ )
413
+ }
414
+
415
+ private readonly requestEnter = () => {
416
+ if (!this.selectedOrganizationId) return
417
+ this.dispatchEvent(
418
+ new CustomEvent('organization-enter-requested', {
419
+ detail: {
420
+ organizationId: this.selectedOrganizationId,
421
+ rememberSelection: this.rememberSelection,
422
+ },
423
+ bubbles: true,
424
+ composed: true,
425
+ }),
426
+ )
427
+ }
428
+ }
429
+
430
+ function normalizeOrganizations(
431
+ organizations: readonly OrganizationSelectorEntry[],
432
+ ): readonly ResolvedOrganizationSelectorEntry[] {
433
+ const seen = new Set<string>()
434
+ return organizations.map((organization) => {
435
+ if (!organization || typeof organization !== 'object') {
436
+ throw new TypeError('organization-selector entries must be objects')
437
+ }
438
+ const id = normalizeRequiredText(organization.id, 'Organization id', 128)
439
+ if (seen.has(id)) throw new TypeError('organization-selector organization ids must be unique')
440
+ seen.add(id)
441
+ const type = organization.type
442
+ if (!['enterprise', 'team'].includes(type)) {
443
+ throw new TypeError('organization-selector organization type is invalid')
444
+ }
445
+ const verificationStatus = normalizeVerificationStatus(type, organization.verificationStatus)
446
+ return {
447
+ id,
448
+ name: normalizeRequiredText(organization.name, 'Organization name', 100),
449
+ type,
450
+ verificationStatus,
451
+ role: normalizeOptionalText(organization.role, 60),
452
+ memberCount: normalizeMemberCount(organization.memberCount),
453
+ lastEnteredLabel: normalizeOptionalText(organization.lastEnteredLabel, 80),
454
+ }
455
+ })
456
+ }
457
+
458
+ function normalizeVerificationStatus(
459
+ type: OrganizationSelectorType,
460
+ value: EnterpriseVerificationStatus | undefined,
461
+ ): EnterpriseVerificationStatus | null {
462
+ if (type === 'team') return null
463
+ if (value === undefined) return 'verified'
464
+ if (!['unverified', 'pending', 'rejected', 'verified', 'expired'].includes(value)) {
465
+ throw new TypeError('organization-selector verification status is invalid')
466
+ }
467
+ return value
468
+ }
469
+
470
+ function normalizeMemberCount(value: unknown): number | null {
471
+ if (value === undefined || value === null) return null
472
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
473
+ throw new TypeError('Organization member count must be a non-negative integer')
474
+ }
475
+ return value
476
+ }
477
+
478
+ function normalizeRequiredText(value: unknown, label: string, maxLength: number): string {
479
+ const normalized = normalizeOptionalText(value, maxLength)
480
+ if (!normalized) throw new TypeError(`${label} is required`)
481
+ return normalized
482
+ }
483
+
484
+ function normalizeOptionalText(value: unknown, maxLength: number): string {
485
+ if (value === undefined || value === null) return ''
486
+ if (typeof value !== 'string')
487
+ throw new TypeError('organization-selector text values must be strings')
488
+ const normalized = value.trim()
489
+ if (normalized.length > maxLength) {
490
+ throw new TypeError('organization-selector text value is too long')
491
+ }
492
+ return normalized
493
+ }
494
+
495
+ function organizationMeta(organization: ResolvedOrganizationSelectorEntry): string {
496
+ const values: string[] = []
497
+ if (organization.role) values.push(organization.role)
498
+ if (organization.memberCount !== null) values.push(`${organization.memberCount} 人`)
499
+ if (organization.lastEnteredLabel) values.push(organization.lastEnteredLabel)
500
+ return values.join(' · ')
501
+ }
502
+
503
+ function organizationBadge(organization: ResolvedOrganizationSelectorEntry) {
504
+ if (organization.type === 'team') return { label: '团队', className: 'team' }
505
+ if (organization.verificationStatus === 'unverified') {
506
+ return { label: '未认证', className: 'unverified' }
507
+ }
508
+ if (organization.verificationStatus === 'pending') {
509
+ return { label: '审核中', className: 'pending' }
510
+ }
511
+ if (organization.verificationStatus === 'rejected') {
512
+ return { label: '未通过', className: 'rejected' }
513
+ }
514
+ if (organization.verificationStatus === 'expired') {
515
+ return { label: '已过期', className: 'expired' }
516
+ }
517
+ return { label: '企业', className: 'enterprise' }
518
+ }
519
+
520
+ function icon(name: OrganizationSelectorType | 'check'): TemplateResult {
521
+ if (name === 'enterprise') {
522
+ return html`<svg viewBox="0 0 24 24" aria-hidden="true">
523
+ <path d="M5 21V6l7-3 7 3v15M3 21h18" />
524
+ <path d="M9 8h1M14 8h1M9 12h1M14 12h1M9 16h1M14 16h1" />
525
+ </svg>`
526
+ }
527
+ if (name === 'team') {
528
+ return html`<svg viewBox="0 0 24 24" aria-hidden="true">
529
+ <circle cx="9" cy="8" r="3" />
530
+ <path d="M3.5 20v-2a5.5 5.5 0 0 1 11 0v2M16 5.5a3 3 0 0 1 0 5.5M17 14a5 5 0 0 1 3.5 4.8V20" />
531
+ </svg>`
532
+ }
533
+ return html`<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12 4 4 10-10" /></svg>`
534
+ }
@@ -0,0 +1,114 @@
1
+ import {
2
+ UniplatOrganizationMultiEmployeeSelectorElement,
3
+ type OrganizationEmployeeSelectionItem,
4
+ type OrganizationMultiEmployeeSelectorDisabledSelection,
5
+ type OrganizationMultiEmployeeSelectorErrorDetail,
6
+ type UniplatOrganizationMultiEmployeeSelectorTransport,
7
+ } from './organization-multi-employee-selector'
8
+
9
+ export type UniplatOrganizationSingleEmployeeSelectorTransport =
10
+ UniplatOrganizationMultiEmployeeSelectorTransport
11
+
12
+ export interface OrganizationSingleEmployeeSelectorConfig {
13
+ applicationName: string
14
+ transport: UniplatOrganizationSingleEmployeeSelectorTransport
15
+ }
16
+
17
+ export type OrganizationSingleEmployeeSelectorDisabledSelection =
18
+ OrganizationMultiEmployeeSelectorDisabledSelection
19
+
20
+ export interface OrganizationSingleEmployeeSelectorOpenOptions {
21
+ title?: string
22
+ initialSelectedEmployeeId?: string
23
+ disabledSelections?: readonly OrganizationSingleEmployeeSelectorDisabledSelection[]
24
+ selectableEmploymentStatuses?: readonly 'active'[]
25
+ }
26
+
27
+ export interface OrganizationSingleEmployeeSelectionConfirmedDetail {
28
+ employeeId: string
29
+ employee: OrganizationEmployeeSelectionItem
30
+ }
31
+
32
+ export type OrganizationSingleEmployeeSelectorErrorDetail =
33
+ OrganizationMultiEmployeeSelectorErrorDetail
34
+
35
+ export interface OrganizationSingleEmployeeSelectorEventMap {
36
+ 'selection-confirmed': CustomEvent<OrganizationSingleEmployeeSelectionConfirmedDetail>
37
+ 'selection-cancelled': CustomEvent<Record<string, never>>
38
+ 'selector-error': CustomEvent<OrganizationSingleEmployeeSelectorErrorDetail>
39
+ }
40
+
41
+ export class UniplatOrganizationSingleEmployeeSelectorElement extends UniplatOrganizationMultiEmployeeSelectorElement {
42
+ protected override get singleSelection(): boolean {
43
+ return true
44
+ }
45
+
46
+ override configure(config: OrganizationSingleEmployeeSelectorConfig): void {
47
+ super.configure(config)
48
+ }
49
+
50
+ override addEventListener<K extends keyof OrganizationSingleEmployeeSelectorEventMap>(
51
+ type: K,
52
+ listener: (
53
+ this: UniplatOrganizationSingleEmployeeSelectorElement,
54
+ event: OrganizationSingleEmployeeSelectorEventMap[K],
55
+ ) => unknown,
56
+ options?: boolean | AddEventListenerOptions,
57
+ ): void
58
+ override addEventListener(
59
+ type: string,
60
+ listener: EventListenerOrEventListenerObject,
61
+ options?: boolean | AddEventListenerOptions,
62
+ ): void
63
+ override addEventListener(
64
+ type: string,
65
+ listener: EventListenerOrEventListenerObject,
66
+ options?: boolean | AddEventListenerOptions,
67
+ ): void {
68
+ super.addEventListener(type, listener, options)
69
+ }
70
+
71
+ override open(options: OrganizationSingleEmployeeSelectorOpenOptions = {}): Promise<void> {
72
+ validateSingleOpenOptions(options)
73
+ return super.open({
74
+ ...(options.title === undefined ? {} : { title: options.title }),
75
+ initialSelectedEmployeeIds:
76
+ options.initialSelectedEmployeeId === undefined ? [] : [options.initialSelectedEmployeeId],
77
+ ...(options.disabledSelections === undefined
78
+ ? {}
79
+ : { disabledSelections: options.disabledSelections }),
80
+ ...(options.selectableEmploymentStatuses === undefined
81
+ ? {}
82
+ : { selectableEmploymentStatuses: options.selectableEmploymentStatuses }),
83
+ })
84
+ }
85
+
86
+ protected override dispatchSelectionConfirmed(
87
+ employees: OrganizationEmployeeSelectionItem[],
88
+ ): void {
89
+ const employee = employees[0]
90
+ if (!employee) return
91
+ this.dispatchEvent(
92
+ new CustomEvent<OrganizationSingleEmployeeSelectionConfirmedDetail>('selection-confirmed', {
93
+ detail: { employeeId: employee.employeeId, employee },
94
+ bubbles: true,
95
+ composed: true,
96
+ }),
97
+ )
98
+ }
99
+ }
100
+
101
+ function validateSingleOpenOptions(options: OrganizationSingleEmployeeSelectorOpenOptions): void {
102
+ if (!options || typeof options !== 'object') {
103
+ throw new TypeError('Organization single employee selector open options are invalid')
104
+ }
105
+ const unsupported = options as OrganizationSingleEmployeeSelectorOpenOptions &
106
+ Record<string, unknown>
107
+ if (
108
+ 'initialSelectedEmployeeIds' in unsupported ||
109
+ 'maxSelection' in unsupported ||
110
+ 'enableDepartmentBulkSelection' in unsupported
111
+ ) {
112
+ throw new TypeError('Multi employee selector options are not supported by the single selector')
113
+ }
114
+ }