@globalbrain/sefirot 3.49.0 → 3.51.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.
@@ -1,8 +1,12 @@
1
1
  <script setup lang="ts">
2
+ import LockKey from '@iconify-icons/ph/lock-key-fill'
2
3
  import IconGoogle from '@iconify-icons/ri/google-fill'
3
- import { computed } from 'vue'
4
+ import { computed, ref } from 'vue'
5
+ import { usePower } from '../composables/Power'
4
6
  import SButton from './SButton.vue'
5
7
  import SLink from './SLink.vue'
8
+ import SLoginPagePasswordDialog from './SLoginPagePasswordDialog.vue'
9
+ import SModal from './SModal.vue'
6
10
  import SIconGbLogoWhite from './icon/SIconGbLogoWhite.vue'
7
11
 
8
12
  export interface CoverTitle {
@@ -15,11 +19,20 @@ export interface CoverPhotographer {
15
19
  link: string
16
20
  }
17
21
 
18
- export interface Action {
22
+ export type Action = ActionPassword | ActionSocial
23
+
24
+ export interface ActionPassword {
25
+ type: 'password'
26
+ onSubmit(email: string, password: string): Promise<boolean>
27
+ }
28
+
29
+ export interface ActionSocial {
19
30
  type: 'google'
20
- onClick: () => Promise<void>
31
+ onClick(): Promise<void>
21
32
  }
22
33
 
34
+ export type ActionType = 'password' | 'google'
35
+
23
36
  const props = defineProps<{
24
37
  cover: string
25
38
  coverTitle: CoverTitle
@@ -27,23 +40,51 @@ const props = defineProps<{
27
40
  actions: Action[]
28
41
  }>()
29
42
 
43
+ const passwordDialog = usePower()
44
+
45
+ const selectedPasswordAction = ref<ActionPassword | null>(null)
46
+ const actionInProgress = ref(false)
47
+ const actionError = ref(false)
48
+
30
49
  const coverBgImageStyle = computed(() => `url(${props.cover})`)
31
50
 
32
- function getActionLabel(type: Action['type']) {
51
+ function getActionLabel(type: ActionType) {
33
52
  switch (type) {
53
+ case 'password':
54
+ return 'Sign in via Password'
34
55
  case 'google':
35
56
  return 'Sign in via Google'
36
- default:
37
- throw new Error('[sefirot] Invalid action type')
38
57
  }
39
58
  }
40
59
 
41
- function getIconComponent(type: Action['type']) {
60
+ function getIconComponent(type: ActionType) {
42
61
  switch (type) {
62
+ case 'password':
63
+ return LockKey
43
64
  case 'google':
44
65
  return IconGoogle
45
- default:
46
- throw new Error('[sefirot] Invalid action type')
66
+ }
67
+ }
68
+
69
+ function onAction(action: Action) {
70
+ switch (action.type) {
71
+ case 'password':
72
+ selectedPasswordAction.value = action
73
+ return passwordDialog.on()
74
+ case 'google':
75
+ return action.onClick()
76
+ }
77
+ }
78
+
79
+ async function onSubmit(email: string, password: string) {
80
+ actionInProgress.value = true
81
+
82
+ actionError.value = !(await selectedPasswordAction.value!.onSubmit(email, password))
83
+
84
+ actionInProgress.value = false
85
+
86
+ if (!actionError.value) {
87
+ passwordDialog.off()
47
88
  }
48
89
  }
49
90
  </script>
@@ -74,20 +115,30 @@ function getIconComponent(type: Action['type']) {
74
115
  <p class="form-lead">This is a very closed login form meant for specific audiences only. If you can’t login, well, you know who to ask.</p>
75
116
  </div>
76
117
 
77
- <div class="form-actions">
78
- <SButton
79
- v-for="action in actions"
80
- :key="action.type"
81
- size="large"
82
- mode="white"
83
- rounded
84
- :label="getActionLabel(action.type)"
85
- :icon="getIconComponent(action.type)"
86
- @click="action.onClick"
87
- />
118
+ <div class="form-actions" :class="{ multi: actions.length > 1 }">
119
+ <div v-for="action in actions" :key="action.type" class="form-action">
120
+ <SButton
121
+ size="large"
122
+ mode="white"
123
+ block
124
+ rounded
125
+ :label="getActionLabel(action.type)"
126
+ :icon="getIconComponent(action.type)"
127
+ @click="onAction(action)"
128
+ />
129
+ </div>
88
130
  </div>
89
131
  </div>
90
132
  </div>
133
+
134
+ <SModal :open="passwordDialog.state.value" @close="passwordDialog.off">
135
+ <SLoginPagePasswordDialog
136
+ :loading="actionInProgress"
137
+ :error="actionError"
138
+ @cancel="passwordDialog.off"
139
+ @submit="onSubmit"
140
+ />
141
+ </SModal>
91
142
  </div>
92
143
  </template>
93
144
 
@@ -196,9 +247,15 @@ function getIconComponent(type: Action['type']) {
196
247
  display: flex;
197
248
  flex-direction: column;
198
249
  align-items: center;
199
- gap: 8px;
250
+ gap: 16px;
200
251
  padding-top: 24px;
201
252
  text-align: center;
202
253
  margin: 0 auto;
203
254
  }
255
+
256
+ .form-actions.multi .form-action {
257
+ display: block;
258
+ width: 100%;
259
+ max-width: 256px;
260
+ }
204
261
  </style>
@@ -0,0 +1,95 @@
1
+ <script setup lang="ts">
2
+ import { useD } from '../composables/D'
3
+ import { useV } from '../composables/V'
4
+ import { email, maxLength, required } from '../validation/rules'
5
+ import SAlert from './SAlert.vue'
6
+ import SCard from './SCard.vue'
7
+ import SCardBlock from './SCardBlock.vue'
8
+ import SContent from './SContent.vue'
9
+ import SControl from './SControl.vue'
10
+ import SControlButton from './SControlButton.vue'
11
+ import SControlRight from './SControlRight.vue'
12
+ import SDoc from './SDoc.vue'
13
+ import SInputText from './SInputText.vue'
14
+
15
+ defineProps<{
16
+ loading: boolean
17
+ error: boolean
18
+ }>()
19
+
20
+ const emit = defineEmits<{
21
+ cancel: []
22
+ submit: [email: string, password: string]
23
+ }>()
24
+
25
+ const { data } = useD({
26
+ email: null as string | null,
27
+ password: null as string | null
28
+ })
29
+
30
+ const { validation, validateAndNotify } = useV(data, {
31
+ email: {
32
+ required: required(),
33
+ maxLength: maxLength(255),
34
+ email: email()
35
+ },
36
+ password: {
37
+ required: required(),
38
+ maxLength: maxLength(255)
39
+ }
40
+ })
41
+
42
+ async function onSubmit() {
43
+ if (await validateAndNotify()) {
44
+ emit('submit', data.value.email!, data.value.password!)
45
+ }
46
+ }
47
+ </script>
48
+
49
+ <template>
50
+ <SCard size="small">
51
+ <SCardBlock class="s-p-24">
52
+ <SDoc>
53
+ <SContent>
54
+ <h2>Sign in to account</h2>
55
+ </SContent>
56
+ <SAlert v-if="error" mode="danger">
57
+ <p>Invalid email or password.</p>
58
+ </SAlert>
59
+ <SInputText
60
+ name="email"
61
+ type="email"
62
+ label="Email"
63
+ placeholder="john.doe@example.com"
64
+ v-model="data.email"
65
+ :validation="validation.email"
66
+ />
67
+ <SInputText
68
+ name="password"
69
+ type="password"
70
+ label="Password"
71
+ placeholder="Password"
72
+ v-model="data.password"
73
+ :validation="validation.password"
74
+ />
75
+ </SDoc>
76
+ </SCardBlock>
77
+ <SCardBlock size="xlarge" class="s-px-24">
78
+ <SControl>
79
+ <SControlRight>
80
+ <SControlButton
81
+ label="Cancel"
82
+ :disabled="loading"
83
+ @click="$emit('cancel')"
84
+ />
85
+ <SControlButton
86
+ mode="info"
87
+ label="Sign in"
88
+ :loading="loading"
89
+ @click="onSubmit"
90
+ />
91
+ </SControlRight>
92
+ </SControl>
93
+ </SCardBlock>
94
+ </SCard>
95
+ </template>
package/lib/http/Http.ts CHANGED
@@ -5,6 +5,8 @@ import { type FetchOptions, type FetchRequest, type FetchResponse, ofetch } from
5
5
  import { stringify } from 'qs'
6
6
  import { type Lang } from '../composables/Lang'
7
7
 
8
+ type Awaitable<T> = T | PromiseLike<T>
9
+
8
10
  export interface HttpClient {
9
11
  <T = any>(request: FetchRequest, options?: Omit<FetchOptions, 'method'>): Promise<T>
10
12
  raw<T = any>(request: FetchRequest, options?: Omit<FetchOptions, 'method'>): Promise<FetchResponse<T>>
@@ -16,6 +18,7 @@ export interface HttpOptions {
16
18
  client?: HttpClient
17
19
  lang?: Lang
18
20
  payloadKey?: string
21
+ headers?: () => Awaitable<Record<string, string>>
19
22
  }
20
23
 
21
24
  export class Http {
@@ -24,6 +27,7 @@ export class Http {
24
27
  private static client: HttpClient = ofetch
25
28
  private static lang: Lang | undefined = undefined
26
29
  private static payloadKey = '__payload__'
30
+ private static headers: () => Awaitable<Record<string, string>> = async () => ({})
27
31
 
28
32
  static config(options: HttpOptions) {
29
33
  if (options.baseUrl) {
@@ -41,6 +45,9 @@ export class Http {
41
45
  if (options.payloadKey) {
42
46
  Http.payloadKey = options.payloadKey
43
47
  }
48
+ if (options.headers) {
49
+ Http.headers = options.headers
50
+ }
44
51
  }
45
52
 
46
53
  private async ensureXsrfToken(): Promise<string | undefined> {
@@ -81,6 +88,7 @@ export class Http {
81
88
  ...options,
82
89
  headers: {
83
90
  Accept: 'application/json',
91
+ ...(await Http.headers()),
84
92
  ...(xsrfToken && { 'X-Xsrf-Token': xsrfToken }),
85
93
  ...(Http.lang && { 'Accept-Language': Http.lang }),
86
94
  ...options.headers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@globalbrain/sefirot",
3
- "version": "3.49.0",
3
+ "version": "3.51.0",
4
4
  "packageManager": "pnpm@9.1.1",
5
5
  "description": "Vue Components for Global Brain Design System.",
6
6
  "author": "Kia Ishii <ka.ishii@globalbrains.com>",