@csntgao/uni-base 0.1.3 → 0.1.4

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.
@@ -3,6 +3,8 @@ import { LitElement, css, html, nothing, type TemplateResult } from 'lit'
3
3
  export type OrganizationSelectorType = 'enterprise' | 'team'
4
4
  export type EnterpriseVerificationStatus =
5
5
  'unverified' | 'pending' | 'rejected' | 'verified' | 'expired'
6
+ export type OrganizationSelectorPresentation = 'inline' | 'modal'
7
+ export type OrganizationSelectorTypeFilter = 'all' | OrganizationSelectorType
6
8
 
7
9
  export interface OrganizationSelectorEntry {
8
10
  id: string
@@ -12,12 +14,16 @@ export interface OrganizationSelectorEntry {
12
14
  role?: string
13
15
  memberCount?: number
14
16
  lastEnteredLabel?: string
17
+ unifiedSocialCreditCode?: string
15
18
  }
16
19
 
17
20
  export interface OrganizationSelectorConfig {
18
21
  organizations: readonly OrganizationSelectorEntry[]
19
22
  selectedOrganizationId?: string
20
23
  rememberSelection?: boolean
24
+ displayName?: string
25
+ maskedMobile?: string
26
+ presentation?: OrganizationSelectorPresentation
21
27
  }
22
28
 
23
29
  export interface OrganizationSelectorEventMap {
@@ -26,6 +32,17 @@ export interface OrganizationSelectorEventMap {
26
32
  rememberSelection: boolean
27
33
  }>
28
34
  'organization-create-requested': CustomEvent<Record<string, never>>
35
+ 'logout-requested': CustomEvent<Record<string, never>>
36
+ }
37
+
38
+ export type OrganizationEntryDecision =
39
+ | { kind: 'personal' }
40
+ | { kind: 'auto-enter'; organizationId: string }
41
+ | { kind: 'prompt'; selectedOrganizationId: string }
42
+
43
+ export interface OrganizationEntryDecisionInput {
44
+ organizations: readonly OrganizationSelectorEntry[]
45
+ rememberedOrganizationId?: string
29
46
  }
30
47
 
31
48
  interface ResolvedOrganizationSelectorEntry {
@@ -36,6 +53,7 @@ interface ResolvedOrganizationSelectorEntry {
36
53
  role: string
37
54
  memberCount: number | null
38
55
  lastEnteredLabel: string
56
+ searchKey: string
39
57
  }
40
58
 
41
59
  export class UniplatOrganizationSelector extends LitElement {
@@ -43,15 +61,22 @@ export class UniplatOrganizationSelector extends LitElement {
43
61
  organizations: { state: true },
44
62
  selectedOrganizationId: { state: true },
45
63
  rememberSelection: { state: true },
64
+ displayName: { state: true },
65
+ maskedMobile: { state: true },
66
+ presentation: { state: true },
67
+ searchQuery: { state: true },
68
+ typeFilter: { state: true },
46
69
  }
47
70
 
48
71
  static override styles = css`
49
72
  :host {
50
- --uniplat-organization-selector-width: 400px;
73
+ --uniplat-organization-selector-width: 420px;
51
74
  --uniplat-organization-selector-primary: #2e6bf5;
52
75
  --uniplat-organization-selector-text: #182634;
53
76
  --uniplat-organization-selector-muted: #7a8794;
54
77
  --uniplat-organization-selector-border: #e4ebf3;
78
+ --uniplat-organization-selector-overlay: rgba(24, 38, 52, 0.55);
79
+ --uniplat-organization-selector-modal-z-index: 1200;
55
80
  --uniplat-organization-selector-font-family:
56
81
  Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
57
82
  display: block;
@@ -61,6 +86,13 @@ export class UniplatOrganizationSelector extends LitElement {
61
86
  line-height: 1.5;
62
87
  }
63
88
 
89
+ :host([data-presentation='modal']) {
90
+ position: fixed;
91
+ z-index: var(--uniplat-organization-selector-modal-z-index);
92
+ inset: 0;
93
+ width: auto;
94
+ }
95
+
64
96
  * {
65
97
  box-sizing: border-box;
66
98
  }
@@ -74,6 +106,20 @@ export class UniplatOrganizationSelector extends LitElement {
74
106
  cursor: pointer;
75
107
  }
76
108
 
109
+ .overlay {
110
+ display: grid;
111
+ width: 100%;
112
+ height: 100%;
113
+ padding: 24px;
114
+ overflow: auto;
115
+ background: var(--uniplat-organization-selector-overlay);
116
+ place-items: center;
117
+ }
118
+
119
+ .overlay .card {
120
+ width: min(var(--uniplat-organization-selector-width), 100%);
121
+ }
122
+
77
123
  .card {
78
124
  width: 100%;
79
125
  padding: 24px;
@@ -89,27 +135,105 @@ export class UniplatOrganizationSelector extends LitElement {
89
135
  margin: 0;
90
136
  }
91
137
 
138
+ .header {
139
+ display: flex;
140
+ align-items: baseline;
141
+ justify-content: space-between;
142
+ gap: 12px;
143
+ }
144
+
92
145
  h2 {
93
- font-size: 20px;
146
+ font-size: 19px;
94
147
  font-weight: 800;
95
148
  line-height: 1.35;
96
149
  }
97
150
 
151
+ .identity {
152
+ flex: none;
153
+ overflow: hidden;
154
+ color: var(--uniplat-organization-selector-muted);
155
+ font-size: 12px;
156
+ text-overflow: ellipsis;
157
+ white-space: nowrap;
158
+ }
159
+
98
160
  .description {
99
161
  margin-top: 7px;
100
162
  color: var(--uniplat-organization-selector-muted);
163
+ font-size: 12.5px;
164
+ }
165
+
166
+ .search {
167
+ display: flex;
168
+ align-items: center;
169
+ gap: 8px;
170
+ margin-top: 15px;
171
+ padding: 0 12px;
172
+ border: 1px solid #e9eef5;
173
+ border-radius: 10px;
174
+ background: #f7f9fc;
175
+ color: #98a4b2;
176
+ }
177
+
178
+ .search input {
179
+ flex: 1;
180
+ min-width: 0;
181
+ min-height: 40px;
182
+ padding: 0;
183
+ border: 0;
184
+ color: var(--uniplat-organization-selector-text);
185
+ background: transparent;
101
186
  font-size: 13px;
187
+ outline: none;
188
+ }
189
+
190
+ .search svg {
191
+ width: 16px;
192
+ height: 16px;
193
+ }
194
+
195
+ .filters {
196
+ display: flex;
197
+ flex-wrap: wrap;
198
+ gap: 8px;
199
+ margin-top: 11px;
200
+ }
201
+
202
+ .chip {
203
+ padding: 4px 11px;
204
+ border: 1px solid #e9eef5;
205
+ border-radius: 999px;
206
+ color: #6b7885;
207
+ background: #f4f7fb;
208
+ font-size: 12px;
209
+ font-weight: 650;
210
+ }
211
+
212
+ .chip.selected {
213
+ border-color: var(--uniplat-organization-selector-primary);
214
+ color: #ffffff;
215
+ background: var(--uniplat-organization-selector-primary);
102
216
  }
103
217
 
104
218
  .list {
105
- display: grid;
106
- gap: 9px;
107
- max-height: 340px;
108
- margin-top: 18px;
219
+ max-height: 392px;
220
+ margin-top: 4px;
109
221
  overflow-y: auto;
110
222
  scrollbar-gutter: stable;
111
223
  }
112
224
 
225
+ .group-label {
226
+ padding: 14px 0 8px;
227
+ color: #98a4b2;
228
+ font-size: 11.5px;
229
+ font-weight: 700;
230
+ }
231
+
232
+ .group {
233
+ display: grid;
234
+ gap: 9px;
235
+ }
236
+
113
237
  .organization {
114
238
  display: grid;
115
239
  grid-template-columns: 36px minmax(0, 1fr) 18px;
@@ -205,18 +329,27 @@ export class UniplatOrganizationSelector extends LitElement {
205
329
  white-space: nowrap;
206
330
  }
207
331
 
208
- .radio {
332
+ .indicator {
209
333
  display: grid;
210
334
  place-items: center;
211
335
  width: 18px;
212
336
  height: 18px;
213
- border: 1.5px solid #c7d1df;
214
- border-radius: 50%;
215
337
  color: transparent;
216
338
  }
217
339
 
218
- .selected .radio {
219
- border-color: var(--uniplat-organization-selector-primary);
340
+ .indicator svg {
341
+ width: 16px;
342
+ height: 16px;
343
+ }
344
+
345
+ .organization:hover .indicator {
346
+ color: #c7d1df;
347
+ }
348
+
349
+ .organization.selected .indicator {
350
+ width: 18px;
351
+ height: 18px;
352
+ border-radius: 50%;
220
353
  color: #ffffff;
221
354
  background: var(--uniplat-organization-selector-primary);
222
355
  }
@@ -254,21 +387,48 @@ export class UniplatOrganizationSelector extends LitElement {
254
387
  font-weight: 650;
255
388
  }
256
389
 
390
+ .actions {
391
+ display: flex;
392
+ align-items: stretch;
393
+ gap: 10px;
394
+ margin-top: 13px;
395
+ }
396
+
397
+ .logout,
257
398
  .enter {
258
399
  display: flex;
259
400
  align-items: center;
260
401
  justify-content: center;
261
- width: 100%;
262
402
  min-height: 46px;
263
- margin-top: 13px;
264
- border: 0;
403
+ padding: 0 14px;
265
404
  border-radius: 10px;
266
- color: #ffffff;
267
- background: var(--uniplat-organization-selector-primary);
268
405
  font-size: 14px;
269
406
  font-weight: 700;
270
407
  }
271
408
 
409
+ .logout {
410
+ flex: none;
411
+ min-width: 104px;
412
+ border: 1px solid #dbe3ed;
413
+ color: #56636f;
414
+ background: #ffffff;
415
+ }
416
+
417
+ .logout:hover {
418
+ background: #f7f9fc;
419
+ }
420
+
421
+ .enter {
422
+ flex: 1;
423
+ min-width: 0;
424
+ overflow: hidden;
425
+ border: 0;
426
+ color: #ffffff;
427
+ background: var(--uniplat-organization-selector-primary);
428
+ text-overflow: ellipsis;
429
+ white-space: nowrap;
430
+ }
431
+
272
432
  .enter:disabled {
273
433
  cursor: not-allowed;
274
434
  background: #a9bff6;
@@ -278,7 +438,7 @@ export class UniplatOrganizationSelector extends LitElement {
278
438
  display: grid;
279
439
  min-height: 120px;
280
440
  place-items: center;
281
- margin-top: 18px;
441
+ margin-top: 14px;
282
442
  border: 1px dashed #dce5f0;
283
443
  border-radius: 10px;
284
444
  color: #8b98a6;
@@ -300,14 +460,25 @@ export class UniplatOrganizationSelector extends LitElement {
300
460
  declare private organizations: readonly ResolvedOrganizationSelectorEntry[]
301
461
  declare private selectedOrganizationId: string
302
462
  declare private rememberSelection: boolean
463
+ declare private displayName: string
464
+ declare private maskedMobile: string
465
+ declare private presentation: OrganizationSelectorPresentation
466
+ declare private searchQuery: string
467
+ declare private typeFilter: OrganizationSelectorTypeFilter
303
468
 
304
469
  private configured = false
470
+ private escapeGuardTarget: Document | null = null
305
471
 
306
472
  constructor() {
307
473
  super()
308
474
  this.organizations = []
309
475
  this.selectedOrganizationId = ''
310
476
  this.rememberSelection = true
477
+ this.displayName = ''
478
+ this.maskedMobile = ''
479
+ this.presentation = 'inline'
480
+ this.searchQuery = ''
481
+ this.typeFilter = 'all'
311
482
  }
312
483
 
313
484
  configure(config: OrganizationSelectorConfig): void {
@@ -319,26 +490,87 @@ export class UniplatOrganizationSelector extends LitElement {
319
490
  this.organizations = organizations
320
491
  this.selectedOrganizationId = organizations.some(({ id }) => id === requestedSelection)
321
492
  ? requestedSelection
322
- : (organizations[0]?.id ?? '')
493
+ : defaultSelectedOrganizationId(organizations)
323
494
  this.rememberSelection = config.rememberSelection ?? true
495
+ this.displayName = normalizeOptionalText(config.displayName, 60)
496
+ this.maskedMobile = normalizeMaskedMobile(config.maskedMobile)
497
+ this.presentation = normalizePresentation(config.presentation)
498
+ this.searchQuery = ''
499
+ this.typeFilter = 'all'
324
500
  this.configured = true
501
+ this.syncPresentation()
325
502
  this.requestUpdate()
326
503
  }
327
504
 
505
+ override connectedCallback(): void {
506
+ super.connectedCallback()
507
+ this.syncPresentation()
508
+ }
509
+
510
+ override disconnectedCallback(): void {
511
+ this.releaseEscapeGuard()
512
+ super.disconnectedCallback()
513
+ }
514
+
328
515
  override render() {
329
516
  if (!this.configured) return nothing
517
+ if (this.presentation === 'modal') {
518
+ return html`<div class="overlay" part="overlay" role="presentation">
519
+ ${this.renderCard(true)}
520
+ </div>`
521
+ }
522
+ return this.renderCard(false)
523
+ }
524
+
525
+ private renderCard(modal: boolean) {
526
+ const visible = this.visibleOrganizations()
527
+ const recent = visible.filter((organization) => organization.lastEnteredLabel !== '')
528
+ const others = visible.filter((organization) => organization.lastEnteredLabel === '')
529
+ const selected = this.organizations.find(({ id }) => id === this.selectedOrganizationId)
330
530
  return html`
331
- <section class="card" part="shell" aria-label="选择团队或企业">
332
- <h2 part="title">选择要进入的团队 / 企业</h2>
531
+ <section
532
+ class="card"
533
+ part="shell"
534
+ role=${modal ? 'dialog' : 'group'}
535
+ aria-modal=${modal ? 'true' : nothing}
536
+ aria-label="选择团队或企业"
537
+ >
538
+ <div class="header">
539
+ <h2 part="title">选择要进入的团队 / 企业</h2>
540
+ ${
541
+ this.identityLabel()
542
+ ? html`<span class="identity" part="identity">${this.identityLabel()}</span>`
543
+ : nothing
544
+ }
545
+ </div>
333
546
  <p class="description" part="description">
334
- 你归属 ${this.organizations.length} 个团队 / 企业,可随时在导航中切换
547
+ 你共归属 ${this.organizations.length} 个组织,进入后可在导航随时切换
335
548
  </p>
549
+ ${this.organizations.length ? this.renderSearch() : nothing}
550
+ ${this.organizations.length ? this.renderFilters() : nothing}
336
551
  ${
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>`
552
+ !this.organizations.length
553
+ ? html`<div class="empty" part="empty">暂无可进入的团队或企业</div>`
554
+ : visible.length
555
+ ? html`<div class="list" part="organization-list">
556
+ ${
557
+ recent.length
558
+ ? html`<div class="group-label" part="group-label">最近进入</div>
559
+ <div class="group">
560
+ ${recent.map((organization) => this.renderOrganization(organization))}
561
+ </div>`
562
+ : nothing
563
+ }
564
+ ${
565
+ others.length
566
+ ? html`<div class="group-label" part="group-label">全部组织</div>
567
+ <div class="group">
568
+ ${others.map((organization) => this.renderOrganization(organization))}
569
+ </div>`
570
+ : nothing
571
+ }
572
+ </div>`
573
+ : html`<div class="empty" part="empty">没有匹配的团队或企业</div>`
342
574
  }
343
575
  <div class="footer-row">
344
576
  <label class="remember" part="remember-selection">
@@ -352,22 +584,74 @@ export class UniplatOrganizationSelector extends LitElement {
352
584
  记住选择,下次直接进入
353
585
  </label>
354
586
  <button class="create" part="create-action" type="button" @click=${this.requestCreate}>
355
- + 创建
587
+ + 创建 / 加入
588
+ </button>
589
+ </div>
590
+ <div class="actions">
591
+ <button class="logout" part="logout-action" type="button" @click=${this.requestLogout}>
592
+ 退出登录
593
+ </button>
594
+ <button
595
+ class="enter"
596
+ part="enter-action"
597
+ type="button"
598
+ ?disabled=${!selected}
599
+ @click=${this.requestEnter}
600
+ >
601
+ ${selected ? `进入「${selected.name}」` : '进入'}
356
602
  </button>
357
603
  </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
604
  </section>
368
605
  `
369
606
  }
370
607
 
608
+ private renderSearch() {
609
+ return html`
610
+ <div class="search" part="search">
611
+ ${icon('search')}
612
+ <input
613
+ type="search"
614
+ part="search-input"
615
+ placeholder="搜索名称,或输入统一社会信用代码"
616
+ aria-label="搜索团队或企业"
617
+ .value=${this.searchQuery}
618
+ @input=${(event: Event) => {
619
+ this.searchQuery = (event.currentTarget as HTMLInputElement).value
620
+ }}
621
+ />
622
+ </div>
623
+ `
624
+ }
625
+
626
+ private renderFilters() {
627
+ const enterprises = this.organizations.filter(({ type }) => type === 'enterprise').length
628
+ const teams = this.organizations.length - enterprises
629
+ const filters: readonly { value: OrganizationSelectorTypeFilter; label: string }[] = [
630
+ { value: 'all', label: `全部 ${this.organizations.length}` },
631
+ { value: 'enterprise', label: `企业 ${enterprises}` },
632
+ { value: 'team', label: `团队 ${teams}` },
633
+ ]
634
+ return html`
635
+ <div class="filters" part="type-filters">
636
+ ${filters.map(
637
+ (filter) => html`
638
+ <button
639
+ class="chip ${this.typeFilter === filter.value ? 'selected' : ''}"
640
+ part="type-filter"
641
+ type="button"
642
+ aria-pressed=${String(this.typeFilter === filter.value)}
643
+ @click=${() => {
644
+ this.typeFilter = filter.value
645
+ }}
646
+ >
647
+ ${filter.label}
648
+ </button>
649
+ `,
650
+ )}
651
+ </div>
652
+ `
653
+ }
654
+
371
655
  private renderOrganization(organization: ResolvedOrganizationSelectorEntry) {
372
656
  const selected = organization.id === this.selectedOrganizationId
373
657
  const badge = organizationBadge(organization)
@@ -397,11 +681,55 @@ export class UniplatOrganizationSelector extends LitElement {
397
681
  </span>
398
682
  <span class="meta">${organizationMeta(organization)}</span>
399
683
  </span>
400
- <span class="radio">${icon('check')}</span>
684
+ <span class="indicator">${icon(selected ? 'check' : 'chevron')}</span>
401
685
  </button>
402
686
  `
403
687
  }
404
688
 
689
+ private identityLabel(): string {
690
+ return [this.displayName, this.maskedMobile].filter((value) => value !== '').join(' · ')
691
+ }
692
+
693
+ private visibleOrganizations(): readonly ResolvedOrganizationSelectorEntry[] {
694
+ const query = this.searchQuery.trim().toLowerCase()
695
+ return this.organizations.filter((organization) => {
696
+ if (this.typeFilter !== 'all' && organization.type !== this.typeFilter) return false
697
+ if (!query) return true
698
+ return organization.searchKey.includes(query)
699
+ })
700
+ }
701
+
702
+ private syncPresentation(): void {
703
+ if (this.configured && this.presentation === 'modal') {
704
+ this.setAttribute('data-presentation', 'modal')
705
+ this.captureEscapeGuard()
706
+ return
707
+ }
708
+ this.removeAttribute('data-presentation')
709
+ this.releaseEscapeGuard()
710
+ }
711
+
712
+ private captureEscapeGuard(): void {
713
+ if (!this.isConnected) return
714
+ const target = this.ownerDocument
715
+ if (this.escapeGuardTarget === target) return
716
+ this.releaseEscapeGuard()
717
+ target.addEventListener('keydown', this.guardEscape, true)
718
+ this.escapeGuardTarget = target
719
+ }
720
+
721
+ private releaseEscapeGuard(): void {
722
+ this.escapeGuardTarget?.removeEventListener('keydown', this.guardEscape, true)
723
+ this.escapeGuardTarget = null
724
+ }
725
+
726
+ // 强制态:Escape 不关闭选择弹窗,也不允许冒泡到宿主的全局关闭逻辑。
727
+ private readonly guardEscape = (event: KeyboardEvent) => {
728
+ if (event.key !== 'Escape') return
729
+ event.preventDefault()
730
+ event.stopPropagation()
731
+ }
732
+
405
733
  private readonly requestCreate = () => {
406
734
  this.dispatchEvent(
407
735
  new CustomEvent('organization-create-requested', {
@@ -412,6 +740,16 @@ export class UniplatOrganizationSelector extends LitElement {
412
740
  )
413
741
  }
414
742
 
743
+ private readonly requestLogout = () => {
744
+ this.dispatchEvent(
745
+ new CustomEvent('logout-requested', {
746
+ detail: {},
747
+ bubbles: true,
748
+ composed: true,
749
+ }),
750
+ )
751
+ }
752
+
415
753
  private readonly requestEnter = () => {
416
754
  if (!this.selectedOrganizationId) return
417
755
  this.dispatchEvent(
@@ -427,6 +765,34 @@ export class UniplatOrganizationSelector extends LitElement {
427
765
  }
428
766
  }
429
767
 
768
+ /**
769
+ * S.0 登录后的入口判定:0 个归属走个人态,仅 1 个归属或命中「记住选择」直接进入,
770
+ * 2 个及以上且未记住时才需要展示选择弹窗。函数无副作用,不读写宿主存储。
771
+ */
772
+ export function resolveOrganizationEntryDecision(
773
+ input: OrganizationEntryDecisionInput,
774
+ ): OrganizationEntryDecision {
775
+ if (!input || !Array.isArray(input.organizations)) {
776
+ throw new TypeError('organization entry decision requires an organizations array')
777
+ }
778
+ const organizations = normalizeOrganizations(input.organizations)
779
+ const first = organizations[0]
780
+ if (!first) return { kind: 'personal' }
781
+ if (organizations.length === 1) return { kind: 'auto-enter', organizationId: first.id }
782
+ const remembered = normalizeOptionalText(input.rememberedOrganizationId, 128)
783
+ if (remembered && organizations.some(({ id }) => id === remembered)) {
784
+ return { kind: 'auto-enter', organizationId: remembered }
785
+ }
786
+ return { kind: 'prompt', selectedOrganizationId: defaultSelectedOrganizationId(organizations) }
787
+ }
788
+
789
+ function defaultSelectedOrganizationId(
790
+ organizations: readonly ResolvedOrganizationSelectorEntry[],
791
+ ): string {
792
+ const recent = organizations.find((organization) => organization.lastEnteredLabel !== '')
793
+ return recent?.id ?? organizations[0]?.id ?? ''
794
+ }
795
+
430
796
  function normalizeOrganizations(
431
797
  organizations: readonly OrganizationSelectorEntry[],
432
798
  ): readonly ResolvedOrganizationSelectorEntry[] {
@@ -443,18 +809,39 @@ function normalizeOrganizations(
443
809
  throw new TypeError('organization-selector organization type is invalid')
444
810
  }
445
811
  const verificationStatus = normalizeVerificationStatus(type, organization.verificationStatus)
812
+ const name = normalizeRequiredText(organization.name, 'Organization name', 100)
813
+ const unifiedSocialCreditCode = normalizeOptionalText(organization.unifiedSocialCreditCode, 32)
446
814
  return {
447
815
  id,
448
- name: normalizeRequiredText(organization.name, 'Organization name', 100),
816
+ name,
449
817
  type,
450
818
  verificationStatus,
451
819
  role: normalizeOptionalText(organization.role, 60),
452
820
  memberCount: normalizeMemberCount(organization.memberCount),
453
821
  lastEnteredLabel: normalizeOptionalText(organization.lastEnteredLabel, 80),
822
+ searchKey: `${name}${unifiedSocialCreditCode}`.toLowerCase(),
454
823
  }
455
824
  })
456
825
  }
457
826
 
827
+ function normalizePresentation(
828
+ value: OrganizationSelectorPresentation | undefined,
829
+ ): OrganizationSelectorPresentation {
830
+ if (value === undefined) return 'inline'
831
+ if (value !== 'inline' && value !== 'modal') {
832
+ throw new TypeError('organization-selector presentation must be inline or modal')
833
+ }
834
+ return value
835
+ }
836
+
837
+ function normalizeMaskedMobile(value: unknown): string {
838
+ const mobile = normalizeOptionalText(value, 40)
839
+ if (/^1[3-9]\d{9}$/.test(mobile)) {
840
+ throw new TypeError('organization-selector only accepts a masked mobile number')
841
+ }
842
+ return mobile
843
+ }
844
+
458
845
  function normalizeVerificationStatus(
459
846
  type: OrganizationSelectorType,
460
847
  value: EnterpriseVerificationStatus | undefined,
@@ -517,7 +904,7 @@ function organizationBadge(organization: ResolvedOrganizationSelectorEntry) {
517
904
  return { label: '企业', className: 'enterprise' }
518
905
  }
519
906
 
520
- function icon(name: OrganizationSelectorType | 'check'): TemplateResult {
907
+ function icon(name: OrganizationSelectorType | 'check' | 'chevron' | 'search'): TemplateResult {
521
908
  if (name === 'enterprise') {
522
909
  return html`<svg viewBox="0 0 24 24" aria-hidden="true">
523
910
  <path d="M5 21V6l7-3 7 3v15M3 21h18" />
@@ -530,5 +917,14 @@ function icon(name: OrganizationSelectorType | 'check'): TemplateResult {
530
917
  <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
918
  </svg>`
532
919
  }
920
+ if (name === 'chevron') {
921
+ return html`<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 6l6 6l-6 6" /></svg>`
922
+ }
923
+ if (name === 'search') {
924
+ return html`<svg viewBox="0 0 24 24" aria-hidden="true">
925
+ <path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" />
926
+ <path d="M21 21l-6 -6" />
927
+ </svg>`
928
+ }
533
929
  return html`<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12 4 4 10-10" /></svg>`
534
930
  }