@widgetic/creator 0.3.54 → 0.3.56

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,45 +1,119 @@
1
1
  <script lang="ts">
2
2
  import { createEventDispatcher } from 'svelte';
3
- import { Tooltip } from '@widgetic/canvas/components';
3
+ import HoverTooltip from './HoverTooltip.svelte';
4
4
 
5
5
  export let name: string = '';
6
6
  export let placeholder: string = 'Unnamed';
7
7
  export let tooltipText: string = 'Double-click to rename';
8
- export let tooltipPosition: 'above' | 'below' | 'left' | 'right' = 'above';
9
8
  let className = '';
10
9
  export { className as class };
11
10
  export let style = '';
12
11
 
13
12
  const dispatch = createEventDispatcher();
14
13
 
14
+ const NAME_DRAG_SLOP_PX = 5;
15
+ const NAME_DBLCLICK_MS = 450;
16
+
15
17
  let isEditing = false;
16
18
  let inputValue = '';
19
+ let nameInputEl: HTMLInputElement | null = null;
20
+ let namePointerStartX = 0;
21
+ let namePointerStartY = 0;
22
+ let namePointerMoved = false;
23
+ let lastNameClickAt = 0;
24
+ let lastNameClickX = 0;
25
+ let lastNameClickY = 0;
26
+
27
+ $: nameDisplayText = isEditing ? inputValue : (name || placeholder);
17
28
 
18
- function startEditing() {
29
+ function openEditMode() {
19
30
  isEditing = true;
20
31
  inputValue = name || '';
21
32
  }
22
33
 
23
- function cancelEditing() {
34
+ function clearNameSelection() {
35
+ requestAnimationFrame(() => {
36
+ if (nameInputEl) {
37
+ const caret = nameInputEl.value.length;
38
+ try {
39
+ nameInputEl.setSelectionRange(caret, caret);
40
+ } catch {
41
+ /* ignore */
42
+ }
43
+ nameInputEl.blur();
44
+ }
45
+ window.getSelection()?.removeAllRanges();
46
+ });
47
+ }
48
+
49
+ function closeEditMode() {
50
+ if (!isEditing) {
51
+ clearNameSelection();
52
+ return;
53
+ }
54
+ const trimmed = inputValue.trim();
24
55
  isEditing = false;
56
+
57
+ if (trimmed && trimmed !== name) {
58
+ dispatch('rename', { newName: trimmed });
59
+ }
25
60
  inputValue = '';
61
+ clearNameSelection();
26
62
  }
27
63
 
28
- function commitEdit() {
64
+ function cancelEditing() {
29
65
  if (!isEditing) return;
30
- const trimmed = inputValue.trim();
31
66
  isEditing = false;
67
+ inputValue = '';
68
+ clearNameSelection();
69
+ }
32
70
 
33
- if (!trimmed || trimmed === name) {
34
- inputValue = '';
71
+ function toggleEditing(e: MouseEvent) {
72
+ e.stopPropagation();
73
+ if (isEditing) {
74
+ closeEditMode();
35
75
  return;
36
76
  }
77
+ openEditMode();
78
+ }
37
79
 
38
- dispatch('rename', { newName: trimmed });
39
- inputValue = '';
80
+ function onNamePointerDown(e: PointerEvent) {
81
+ if (isEditing) {
82
+ e.stopPropagation();
83
+ return;
84
+ }
85
+ namePointerMoved = false;
86
+ namePointerStartX = e.clientX;
87
+ namePointerStartY = e.clientY;
88
+ }
89
+
90
+ function onNamePointerMove(e: PointerEvent) {
91
+ if (isEditing || namePointerMoved) return;
92
+ if (Math.hypot(e.clientX - namePointerStartX, e.clientY - namePointerStartY) > NAME_DRAG_SLOP_PX) {
93
+ namePointerMoved = true;
94
+ }
95
+ }
96
+
97
+ function onNamePointerUp(e: PointerEvent) {
98
+ if (isEditing) return;
99
+ if (namePointerMoved) {
100
+ lastNameClickAt = 0;
101
+ return;
102
+ }
103
+ const now = Date.now();
104
+ const dist = Math.hypot(e.clientX - lastNameClickX, e.clientY - lastNameClickY);
105
+ if (now - lastNameClickAt <= NAME_DBLCLICK_MS && dist <= 10) {
106
+ lastNameClickAt = 0;
107
+ openEditMode();
108
+ return;
109
+ }
110
+ lastNameClickAt = now;
111
+ lastNameClickX = e.clientX;
112
+ lastNameClickY = e.clientY;
40
113
  }
41
114
 
42
115
  function focusAndSelect(node: HTMLInputElement, editing: boolean) {
116
+ nameInputEl = node;
43
117
  function apply(shouldEdit: boolean) {
44
118
  if (shouldEdit) {
45
119
  requestAnimationFrame(() => {
@@ -58,37 +132,76 @@
58
132
  </script>
59
133
 
60
134
  <!-- svelte-ignore a11y-no-static-element-interactions -->
61
- <Tooltip text={isEditing ? '' : tooltipText} position={tooltipPosition}>
135
+ <HoverTooltip text={isEditing ? '' : tooltipText} class="min-w-0 w-max max-w-full" {style}>
62
136
  <div
63
- class="editable-name-ct flex items-center gap-1 min-w-0 {className}"
64
- {style}
65
- ondblclick={() => startEditing()}
137
+ class="editable-name-ct flex items-center w-max max-w-full min-w-0 {isEditing ? 'gap-0.5' : 'gap-0'} {className}"
138
+ onpointerdown={onNamePointerDown}
139
+ onpointermove={onNamePointerMove}
140
+ onpointerup={onNamePointerUp}
141
+ onpointercancel={() => { namePointerMoved = true; }}
66
142
  >
67
- <input
68
- class="editable-name-input min-w-0 w-full overflow-hidden text-ellipsis whitespace-nowrap font-semibold rounded px-1 py-0.5 text-sm outline-none transition-colors {isEditing ? 'border border-blue-400 bg-white focus:ring-1 focus:ring-blue-500' : 'border border-transparent bg-transparent cursor-default'}"
69
- style="flex: 1 1 0%; {!isEditing ? 'pointer-events: none;' : ''}"
70
- type="text"
71
- value={isEditing ? inputValue : (name || placeholder)}
72
- readonly={!isEditing}
73
- tabindex={isEditing ? 0 : -1}
74
- use:focusAndSelect={isEditing}
75
- oninput={(e) => { if (isEditing) inputValue = e.currentTarget.value; }}
76
- onkeydown={(e) => {
77
- if (!isEditing) return;
78
- e.stopPropagation();
79
- if (e.key === 'Enter') commitEdit();
80
- if (e.key === 'Escape') cancelEditing();
81
- }}
82
- onblur={() => { if (isEditing) commitEdit(); }}
83
- />
84
- {#if !isEditing}
85
- <span class="editable-name-pencil flex-shrink-0 text-gray-400 opacity-0 text-xs cursor-pointer transition-opacity">✏️</span>
86
- {/if}
143
+ <div class="editable-name-input-wrap relative min-w-0 max-w-full">
144
+ <span class="editable-name-sizer" aria-hidden="true">{nameDisplayText}</span>
145
+ <input
146
+ class="editable-name-input absolute inset-0 min-w-0 overflow-hidden text-ellipsis whitespace-nowrap font-semibold rounded text-sm outline-none transition-colors {isEditing ? 'editable-name-input-editing border border-blue-400 bg-white focus:ring-1 focus:ring-blue-500' : 'editable-name-input-idle border border-transparent bg-transparent cursor-grab'}"
147
+ style="{!isEditing ? 'pointer-events: none;' : ''}"
148
+ type="text"
149
+ value={nameDisplayText}
150
+ readonly={!isEditing}
151
+ tabindex={isEditing ? 0 : -1}
152
+ use:focusAndSelect={isEditing}
153
+ oninput={(e) => { if (isEditing) inputValue = e.currentTarget.value; }}
154
+ onkeydown={(e) => {
155
+ if (!isEditing) return;
156
+ e.stopPropagation();
157
+ if (e.key === 'Enter') closeEditMode();
158
+ if (e.key === 'Escape') cancelEditing();
159
+ }}
160
+ onblur={() => { if (isEditing) closeEditMode(); }}
161
+ />
162
+ </div>
163
+ <button
164
+ type="button"
165
+ class="editable-name-pencil w-4 h-4 shrink-0 text-gray-400 text-[16px] leading-none cursor-pointer transition-opacity flex items-center justify-center p-0 bg-transparent border-0 {isEditing ? 'opacity-100' : 'opacity-0'}"
166
+ aria-label={isEditing ? 'Save name' : 'Rename widget'}
167
+ onmousedown={(e) => { e.preventDefault(); e.stopPropagation(); }}
168
+ onpointerdown={(e) => e.stopPropagation()}
169
+ onclick={toggleEditing}
170
+ >✏️</button>
87
171
  </div>
88
- </Tooltip>
172
+ </HoverTooltip>
89
173
 
90
174
  <style>
91
- .editable-name-ct:hover .editable-name-pencil {
175
+ .editable-name-sizer {
176
+ display: block;
177
+ visibility: hidden;
178
+ white-space: nowrap;
179
+ font: inherit;
180
+ padding: 2px 0 2px 2px;
181
+ }
182
+ .editable-name-ct:focus-within .editable-name-sizer {
183
+ padding-right: 2px;
184
+ }
185
+ .editable-name-input {
186
+ width: 100%;
187
+ height: 100%;
188
+ padding: 2px 0 2px 2px;
189
+ margin: 0;
190
+ border-width: 1px;
191
+ box-sizing: border-box;
192
+ }
193
+ .editable-name-ct:hover .editable-name-pencil,
194
+ .editable-name-ct:focus-within .editable-name-pencil {
92
195
  opacity: 1;
93
196
  }
197
+ .editable-name-ct:hover .editable-name-input-idle {
198
+ text-decoration: underline;
199
+ text-decoration-style: dotted;
200
+ text-underline-offset: 3px;
201
+ }
202
+ .editable-name-pencil:focus,
203
+ .editable-name-pencil:focus-visible {
204
+ outline: none;
205
+ box-shadow: none;
206
+ }
94
207
  </style>
@@ -15,7 +15,6 @@ declare const EditableName: $$__sveltets_2_IsomorphicComponent<{
15
15
  name?: string;
16
16
  placeholder?: string;
17
17
  tooltipText?: string;
18
- tooltipPosition?: "above" | "below" | "left" | "right";
19
18
  class?: string;
20
19
  style?: string;
21
20
  }, {
@@ -0,0 +1,60 @@
1
+ <script lang="ts">
2
+ import { Portal } from '@widgetic/design-system/components';
3
+
4
+ export let text: string = '';
5
+ let className = '';
6
+ export { className as class };
7
+ export let style = '';
8
+
9
+ let triggerEl: HTMLElement | null = null;
10
+ let open = false;
11
+ let tipTop = 0;
12
+ let tipLeft = 0;
13
+ const portalTarget = `wc-hover-tooltip-${Math.random().toString(36).slice(2, 10)}`;
14
+
15
+ function place() {
16
+ if (!triggerEl) return;
17
+ const rect = triggerEl.getBoundingClientRect();
18
+ tipTop = rect.top - 8;
19
+ tipLeft = rect.left + rect.width / 2;
20
+ }
21
+
22
+ function show() {
23
+ if (!text) return;
24
+ place();
25
+ open = true;
26
+ }
27
+
28
+ function hide() {
29
+ open = false;
30
+ }
31
+ </script>
32
+
33
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
34
+ <div
35
+ bind:this={triggerEl}
36
+ class="wc-hover-tooltip-trigger {className}"
37
+ {style}
38
+ onmouseenter={show}
39
+ onmouseleave={hide}
40
+ onfocusin={show}
41
+ onfocusout={hide}
42
+ >
43
+ <slot />
44
+ </div>
45
+
46
+ {#if open && text}
47
+ <Portal target={portalTarget}>
48
+ <div
49
+ class="wc-hover-tooltip-bubble pointer-events-none fixed z-[99999] -translate-x-1/2 -translate-y-full rounded-md bg-[#1f2937] px-2.5 py-1 text-xs leading-snug text-[#f9fafb] whitespace-nowrap shadow-md"
50
+ style="top: {tipTop}px; left: {tipLeft}px;"
51
+ role="tooltip"
52
+ >
53
+ {text}
54
+ <span
55
+ class="wc-hover-tooltip-arrow absolute left-1/2 top-full h-0 w-0 -translate-x-1/2 border-x-[5px] border-t-[5px] border-x-transparent border-t-[#1f2937]"
56
+ aria-hidden="true"
57
+ ></span>
58
+ </div>
59
+ </Portal>
60
+ {/if}
@@ -0,0 +1,31 @@
1
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
+ $$bindings?: Bindings;
4
+ } & Exports;
5
+ (internal: unknown, props: Props & {
6
+ $$events?: Events;
7
+ $$slots?: Slots;
8
+ }): Exports & {
9
+ $set?: any;
10
+ $on?: any;
11
+ };
12
+ z_$$bindings?: Bindings;
13
+ }
14
+ type $$__sveltets_2_PropsWithChildren<Props, Slots> = Props & (Slots extends {
15
+ default: any;
16
+ } ? Props extends Record<string, never> ? any : {
17
+ children?: any;
18
+ } : {});
19
+ declare const HoverTooltip: $$__sveltets_2_IsomorphicComponent<$$__sveltets_2_PropsWithChildren<{
20
+ text?: string;
21
+ class?: string;
22
+ style?: string;
23
+ }, {
24
+ default: {};
25
+ }>, {
26
+ [evt: string]: CustomEvent<any>;
27
+ }, {
28
+ default: {};
29
+ }, {}, string>;
30
+ type HoverTooltip = InstanceType<typeof HoverTooltip>;
31
+ export default HoverTooltip;