@operato/contact 8.0.0-beta.0 → 8.0.0-beta.2

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,296 +0,0 @@
1
- import '@material/web/icon/icon.js'
2
- import { html, css, LitElement, PropertyValues } from 'lit'
3
- import { customElement, property, query } from 'lit/decorators.js'
4
- import { Profile } from './ox-pfp-view'
5
- import { FileDropHelper } from '@operato/utils'
6
- import { gesture, GestureEventDoubleTap, GestureEventDrag, GestureEventPinch } from '@operato/utils/gesture-mixin.js'
7
-
8
- @customElement('ox-pfp-edit')
9
- export class OxPfpEdit extends gesture(LitElement) {
10
- static styles = css`
11
- :host {
12
- display: inline-block;
13
- overflow: hidden;
14
- aspect-ratio: 1 / 1;
15
- height: var(--ox-pfp-size, 100px);
16
- width: var(--ox-pfp-size, 100px);
17
- touch-action: none;
18
- }
19
-
20
- [content] {
21
- display: flex;
22
- align-items: center;
23
- justify-content: center;
24
- width: 100%;
25
- height: 100%;
26
- }
27
-
28
- [circle] {
29
- width: 90%;
30
- height: 90%;
31
- position: relative;
32
- }
33
-
34
- [circle]::after {
35
- position: absolute;
36
- content: '';
37
- width: 100%;
38
- height: 100%;
39
- box-shadow: 0 0 0 1000px lightgray;
40
- border-radius: 50%;
41
- opacity: 0.9;
42
- }
43
-
44
- [circle]::before {
45
- content: '';
46
- display: block;
47
- position: absolute;
48
- text-align: left;
49
- line-height: 24px;
50
- top: 0;
51
- right: 0;
52
- width: 100%;
53
- height: 100%;
54
- background-repeat: no-repeat;
55
- background-image: var(
56
- --background-image,
57
- linear-gradient(
58
- var(--md-sys-color-on-primary-container, lightgray),
59
- var(--md-sys-color-on-primary-container, lightgray)
60
- )
61
- );
62
- transform: var(--image-transform, '');
63
- background-size: 100% 100%;
64
- }
65
-
66
- [monogram] {
67
- width: 100%;
68
- height: 100%;
69
- border-radius: 50%;
70
- border: 1px solid darkgray;
71
- box-sizing: border-box;
72
- padding: 0;
73
- overflow: hidden;
74
- background-color: gray;
75
- color: white;
76
- display: flex;
77
- align-items: center;
78
- justify-content: center;
79
- }
80
-
81
- [monogram] div {
82
- flex: 1;
83
- text-transform: uppercase;
84
- font-size: 2em;
85
- font-family: system-ui;
86
- text-align: center;
87
- }
88
-
89
- [file-load] {
90
- position: absolute;
91
- left: 0;
92
- top: 0;
93
- display: flex;
94
- place-content: center;
95
- }
96
-
97
- [file-load]:hover {
98
- opacity: 50%;
99
- }
100
-
101
- #input-file {
102
- display: none;
103
- }
104
-
105
- label {
106
- display: flex;
107
- align-content: center;
108
- }
109
-
110
- md-icon {
111
- align-self: center;
112
- color: var(--file-uploader-icon-color, black);
113
- --md-icon-size: var(--file-uploader-icon-size, 24px);
114
- }
115
- `
116
-
117
- @property({ type: Object }) profile: Profile = {}
118
- @property({ type: String }) name?: string
119
-
120
- @query('[circle]') circle!: HTMLDivElement
121
-
122
- connectedCallback(): void {
123
- super.connectedCallback()
124
-
125
- FileDropHelper.set(this)
126
-
127
- this.addEventListener('pinch', this.pinchHandler as EventListener)
128
- this.addEventListener('drag', this.dragHandler as EventListener)
129
- this.addEventListener('doubletap', this.doubleTapHandler as EventListener)
130
- this.addEventListener('wheel', this.wheelHandler as EventListener)
131
- this.addEventListener('file-drop', this.dropFileHandler)
132
- }
133
-
134
- disconnectedCallback() {
135
- this.removeEventListener('pinch', this.pinchHandler as EventListener)
136
- this.removeEventListener('drag', this.dragHandler as EventListener)
137
- this.removeEventListener('doubletap', this.doubleTapHandler as EventListener)
138
- this.removeEventListener('wheel', this.wheelHandler as EventListener)
139
- this.removeEventListener('file-drop', this.dropFileHandler)
140
- super.disconnectedCallback()
141
- }
142
-
143
- render() {
144
- return html`
145
- <div content>
146
- ${this.profile?.picture
147
- ? html`<div circle></div>`
148
- : html`<div monogram><div>${this.name?.replace(/\s/g, '').slice(0, 2)}</div></div>`}
149
- </div>
150
-
151
- <div file-load>
152
- <label>
153
- <input
154
- id="input-file"
155
- type="file"
156
- accept="image/*"
157
- hidden
158
- capture="environment"
159
- @change=${(e: Event) => this.onChangeFile(e)}
160
- />
161
- <md-icon title="select image">image_search</md-icon>
162
- </label>
163
- </div>
164
-
165
- <slot></slot>
166
- `
167
- }
168
-
169
- updated(changes: PropertyValues<this>) {
170
- const { picture, zoom = 1, left = 0, top = 0 } = this.profile || {}
171
-
172
- if (picture) {
173
- const style = this.style
174
- style.setProperty('--background-image', `url('${picture}')`)
175
- style.setProperty('--image-transform', `translate(${left}%, ${top}%) scale(${zoom}, ${zoom})`)
176
- }
177
- }
178
-
179
- get value() {
180
- return this.profile
181
- }
182
-
183
- set value(value: Profile) {
184
- this.profile = value
185
- }
186
-
187
- private doubleTapHandler = (e: GestureEventDoubleTap) => {
188
- this.fit()
189
- }
190
-
191
- private wheelHandler = (e: WheelEvent) => {
192
- e.preventDefault()
193
-
194
- const delta = Math.max(-1, Math.min(1, e.deltaY || -e.detail))
195
- const scale = 1 - delta / 20
196
-
197
- // wheel 이벤트가 발생한 포인터 위치를 중심으로 줌
198
- this.zoomAt(e.clientX, e.clientY, scale)
199
- }
200
-
201
- dragHandler = (e: GestureEventDrag) => {
202
- const { deltaX, deltaY } = e.detail
203
- const { offsetHeight, offsetWidth } = this
204
- const { top = 0, left = 0, zoom = 1, picture, file } = this.profile || {}
205
-
206
- this.profile = {
207
- top: top + (deltaY / offsetHeight) * 100,
208
- left: left + (deltaX / offsetWidth) * 100,
209
- zoom,
210
- picture,
211
- file
212
- }
213
-
214
- this.dispatchEvent(
215
- new CustomEvent('change', {
216
- detail: this.profile
217
- })
218
- )
219
- }
220
-
221
- pinchHandler = (e: GestureEventPinch) => {
222
- const { scale, centerX, centerY } = e.detail
223
- this.zoomAt(centerX, centerY, scale)
224
- }
225
-
226
- /**
227
- * 중심점을 기준으로 확대/축소
228
- */
229
- private zoomAt(clientX: number, clientY: number, scale: number) {
230
- const { offsetLeft, offsetTop, offsetWidth: width, offsetHeight: height } = this
231
-
232
- const centerX = offsetLeft + width / 2
233
- const centerY = offsetTop + height / 2
234
-
235
- const { top = 0, left = 0, zoom = 1, picture, file } = this.profile || {}
236
-
237
- const rect = this.getBoundingClientRect()
238
-
239
- const relativeX = (centerX - rect.left) / this.offsetWidth
240
- const relativeY = (centerY - rect.top) / this.offsetHeight
241
-
242
- const newZoom = zoom * scale
243
-
244
- const deltaX = (relativeX * this.offsetWidth * (1 - scale)) / newZoom
245
- const deltaY = (relativeY * this.offsetHeight * (1 - scale)) / newZoom
246
-
247
- this.profile = {
248
- top: top - (deltaY / this.offsetHeight) * 100,
249
- left: left - (deltaX / this.offsetWidth) * 100,
250
- zoom: newZoom,
251
- picture,
252
- file
253
- }
254
-
255
- this.dispatchEvent(
256
- new CustomEvent('change', {
257
- detail: this.profile
258
- })
259
- )
260
- }
261
-
262
- dropFileHandler = (e: Event) => {
263
- const dropFiles = (e as CustomEvent).detail
264
- const imageFile = dropFiles[0]
265
- if (!imageFile) return
266
-
267
- const picture = URL.createObjectURL(imageFile)
268
- this.profile = { left: 0, top: 0, zoom: 1, picture, file: imageFile }
269
- }
270
-
271
- onChangeFile(e: Event) {
272
- const fileInput = e.currentTarget as HTMLInputElement
273
- const imageFile = [...Array.from(fileInput.files as FileList)][0]
274
- if (!imageFile) return
275
-
276
- const picture = URL.createObjectURL(imageFile)
277
- this.profile = { left: 0, top: 0, zoom: 1, picture, file: imageFile }
278
-
279
- fileInput.value = '' // Clear file input after use
280
- }
281
-
282
- fit() {
283
- // Fit the image to the full container size and centered
284
- this.profile = { ...this.profile, left: 0, top: 0, zoom: 1 }
285
-
286
- this.dispatchEvent(
287
- new CustomEvent('change', {
288
- detail: this.profile
289
- })
290
- )
291
- }
292
-
293
- clear() {
294
- this.profile = {}
295
- }
296
- }
@@ -1,104 +0,0 @@
1
- import { html, css, LitElement, PropertyValues } from 'lit'
2
- import { customElement, property, query, state } from 'lit/decorators.js'
3
-
4
- export type Profile = {
5
- left?: number
6
- top?: number
7
- zoom?: number
8
- picture?: string
9
- file?: File
10
- }
11
-
12
- @customElement('ox-pfp-view')
13
- export class OxPfpView extends LitElement {
14
- static styles = css`
15
- :host {
16
- display: inline-block;
17
- aspect-ratio: 1 / 1;
18
-
19
- height: var(--ox-pfp-size, 100px);
20
- width: var(--ox-pfp-size, 100px);
21
- }
22
-
23
- [circle] {
24
- width: 100%;
25
- height: 100%;
26
- border-radius: 50%;
27
- position: relative;
28
- background-color: var(--md-sys-color-on-primary-container, #999);
29
-
30
- overflow: hidden;
31
- }
32
-
33
- [circle]::before {
34
- content: '';
35
- display: block;
36
- position: absolute;
37
- text-align: left;
38
- line-height: 24px;
39
- top: 0;
40
- right: 0;
41
- width: 100%;
42
- height: 100%;
43
- background-repeat: no-repeat;
44
- background-image: var(
45
- --background-image,
46
- linear-gradient(
47
- var(--md-sys-color-on-primary-container, #999),
48
- var(--md-sys-color-on-secondary-container, #666)
49
- )
50
- );
51
- transform: var(--image-transform, '');
52
- background-size: 100% 100%;
53
- }
54
-
55
- [monogram] {
56
- height: 100%;
57
- border-radius: 50%;
58
- border: 1px solid rgba(0, 0, 0, 0.1);
59
- box-sizing: border-box;
60
- padding: 0;
61
- overflow: hidden;
62
- background-color: var(--md-sys-color-primary-container, #999);
63
- color: var(--md-sys-color-on-primary-container, white);
64
-
65
- display: flex;
66
- align-items: center;
67
- justify-content: center;
68
- }
69
-
70
- [monogram] div {
71
- flex: 1;
72
- text-transform: uppercase;
73
- font-size: 1.5em;
74
- font-family: system-ui;
75
- text-align: center;
76
- }
77
- `
78
-
79
- @property({ type: Object }) profile?: Profile = {}
80
- @property({ type: String }) name?: string
81
-
82
- @query('[circle]') circle!: HTMLDivElement
83
-
84
- render() {
85
- return html`
86
- ${this.profile?.picture
87
- ? html`<div circle></div>`
88
- : html`<div monogram><div>${this.name?.replace(/\s/g, '').slice(0, 2)}</div></div>`}
89
-
90
- <slot></slot>
91
- `
92
- }
93
-
94
- updated(changes: PropertyValues<this>) {
95
- const { picture, zoom = 1, left = 0, top = 0 } = this.profile || {}
96
-
97
- if (picture) {
98
- const style = this.style
99
-
100
- style.setProperty('--background-image', `url(${picture})`)
101
- style.setProperty('--image-transform', `translate(${left}%, ${top}%) scale(${zoom}, ${zoom})`)
102
- }
103
- }
104
- }
package/src/ox-pfp.ts DELETED
@@ -1,95 +0,0 @@
1
- import '@material/web/icon/icon.js'
2
-
3
- import './ox-pfp-edit.js'
4
- import './ox-pfp-view.js'
5
-
6
- import { html, css, LitElement } from 'lit'
7
- import { customElement, property, query, state } from 'lit/decorators.js'
8
- import { Profile } from './ox-pfp-view.js'
9
- import { OxPfpEdit } from './ox-pfp-edit.js'
10
-
11
- @customElement('ox-pfp')
12
- export class OxPfp extends LitElement {
13
- static styles = css`
14
- :host {
15
- display: block;
16
- aspect-ratio: 1 / 1;
17
-
18
- height: var(--ox-pfp-size, 100px);
19
- width: var(--ox-pfp-size, 100px);
20
- }
21
-
22
- ox-pfp-edit,
23
- ox-pfp-view {
24
- position: relative;
25
- }
26
-
27
- [buttons] {
28
- position: absolute;
29
- bottom: 0;
30
- right: 0;
31
- display: flex;
32
- }
33
-
34
- md-icon {
35
- position: absolute;
36
-
37
- color: var(--md-sys-color-primary);
38
- --md-icon-size: 24px;
39
- }
40
-
41
- md-icon[icon='save'] {
42
- bottom: 0;
43
- right: 0;
44
- }
45
-
46
- md-icon[icon='edit'] {
47
- bottom: 0;
48
- right: 0;
49
- }
50
-
51
- md-icon[icon='cancel'] {
52
- top: 0;
53
- right: 0;
54
- }
55
-
56
- md-icon[icon='clear'] {
57
- bottom: 0;
58
- left: 0;
59
- }
60
- `
61
-
62
- @property({ type: Object }) profile: Profile = {}
63
- @property({ type: String }) name?: string
64
-
65
- @state() editMode?: boolean = false
66
-
67
- @query('ox-pfp-edit') edit!: OxPfpEdit
68
-
69
- render() {
70
- return this.editMode
71
- ? html`<ox-pfp-edit .profile=${this.profile} name=${this.name || ''}>
72
- <md-icon title="clear" icon="clear" @click=${() => this.edit.clear()}>delete</md-icon>
73
- <md-icon
74
- title="save"
75
- icon="save"
76
- @click=${() => {
77
- this.profile = this.edit.profile
78
- this.editMode = false
79
- this.dispatchEvent(
80
- new CustomEvent('change', {
81
- detail: this.profile
82
- })
83
- )
84
- }}
85
- >save</md-icon
86
- >
87
- <md-icon title="cancel" icon="cancel" @click=${() => (this.editMode = false)}>cancel</md-icon>
88
- </ox-pfp-edit> `
89
- : html`
90
- <ox-pfp-view .profile=${this.profile} name=${this.name || ''}>
91
- <md-icon title="edit" icon="edit" @click=${() => (this.editMode = true)}>edit</md-icon>
92
- </ox-pfp-view>
93
- `
94
- }
95
- }
@@ -1,107 +0,0 @@
1
- import { html, TemplateResult } from 'lit'
2
- import '@material/web/icon/icon.js'
3
- import '@operato/i18n'
4
- import '../src/ox-contact.js'
5
-
6
- import { Contact, ContactField } from '../src/ox-contact.js'
7
-
8
- export default {
9
- title: 'OxContact',
10
- component: 'ox-contact',
11
- argTypes: {
12
- contact: { control: 'object' }
13
- }
14
- }
15
-
16
- interface Story<T> {
17
- (args: T): TemplateResult
18
- args?: Partial<T>
19
- argTypes?: Record<string, unknown>
20
- }
21
-
22
- interface ArgTypes {
23
- contact?: Contact
24
- }
25
-
26
- const Template: Story<ArgTypes> = ({ contact }: ArgTypes) => html`
27
- <link
28
- href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL@20..48,100..700,0..1"
29
- rel="stylesheet"
30
- />
31
- <link
32
- href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL@20..48,100..700,0..1"
33
- rel="stylesheet"
34
- />
35
- <link
36
- href="https://fonts.googleapis.com/css2?family=Material+Symbols+Sharp:opsz,wght,FILL@20..48,100..700,0..1"
37
- rel="stylesheet"
38
- />
39
-
40
- <link href="/themes/app-theme.css" rel="stylesheet" />
41
- <link href="/themes/light.css" rel="stylesheet" />
42
- <link href="/themes/dark.css" rel="stylesheet" />
43
- <link href="/themes/spacing.css" rel="stylesheet" />
44
- <link href="/themes/grist-theme.css" rel="stylesheet" />
45
- <link href="/themes/form-theme.css" rel="stylesheet" />
46
-
47
- <style>
48
- body {
49
- background-color: var(--md-sys-color-surface);
50
- }
51
- </style>
52
-
53
- <ox-contact .contact=${contact}> </ox-contact>
54
- `
55
-
56
- export const Regular = Template.bind({})
57
- Regular.args = {
58
- contact: {
59
- note: 'abcdefg',
60
- name: '남 상혁',
61
- company: 'Hatiolab',
62
- items: [
63
- {
64
- id: '2',
65
- type: ContactField.Phone,
66
- label: 'home',
67
- value: '010-1223-3443'
68
- },
69
- {
70
- id: '3',
71
- type: ContactField.Phone,
72
- label: 'mobile',
73
- value: '010-1122-3344'
74
- },
75
- {
76
- id: '4',
77
- type: ContactField.Email,
78
- label: 'work',
79
- value: 'heartyoh@hatiolab.com'
80
- },
81
- {
82
- id: '5',
83
- type: ContactField.Email,
84
- label: 'home',
85
- value: 'shnam2k@gmail.com'
86
- },
87
- {
88
- id: '6',
89
- type: ContactField.Address,
90
- label: 'home',
91
- value: '경기도 성남시 분당구 123'
92
- },
93
- {
94
- id: '7',
95
- type: ContactField.Address,
96
- label: 'work',
97
- value: '경기도 성남시 분당구 234'
98
- },
99
- {
100
- id: '8',
101
- type: ContactField.Address,
102
- label: 'work',
103
- value: '서울특별시 종로구 123'
104
- }
105
- ]
106
- }
107
- }