@geode/opengeodeweb-front 10.34.0-rc.14 → 10.34.0-rc.15

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,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import GlassCard from "@ogw_front/components/GlassCard.vue";
3
3
  import { middleTruncate } from "@ogw_front/utils/string";
4
- import { useClipboard } from "@vueuse/core";
4
+ import { useCopyToClipboard } from "@ogw_front/composables/copy_to_clipboard";
5
5
  import { useDataStore } from "@ogw_front/stores/data";
6
6
  import { useMenuStore } from "@ogw_front/stores/menu";
7
7
 
@@ -17,7 +17,6 @@ const emit = defineEmits<{
17
17
  "update:show": [value: boolean];
18
18
  }>();
19
19
 
20
- const COPIED_TIMEOUT = 1500;
21
20
  const MAX_SHORT_ID_LENGTH = 15;
22
21
  const ID_SLICE_START = 8;
23
22
  const ID_SLICE_END_OFFSET = 7;
@@ -25,7 +24,7 @@ const TRUNCATE_MAX_LENGTH = 22;
25
24
  const TRUNCATE_START_CHARS = 12;
26
25
  const TRUNCATE_END_CHARS = 7;
27
26
 
28
- const { copy, copied } = useClipboard({ copiedDuring: COPIED_TIMEOUT });
27
+ const { copy, copied } = useCopyToClipboard();
29
28
  const copiedId = ref("");
30
29
 
31
30
  function isCopied(id: string | undefined): boolean {
@@ -1,6 +1,8 @@
1
1
  <script setup lang="ts">
2
- // oxlint-disable id-length
2
+ // oxlint-disable id-length since vuetify require { r,g,b} format to work
3
+ import { formatColorString, parseColorString } from "@ogw_front/utils/color_picker";
3
4
  import type { RGBAColor } from "@ogw_front/utils/default_styles/constants";
5
+ import { useCopyToClipboard } from "@ogw_front/composables/copy_to_clipboard";
4
6
 
5
7
  interface Props {
6
8
  disabledAlpha?: boolean;
@@ -8,12 +10,15 @@ interface Props {
8
10
 
9
11
  const { disabledAlpha = false } = defineProps<Props>();
10
12
 
11
- // The useMousePressed composable only needs the underlying DOM element (it unwraps a component ref's $el at runtime); typing this as the actual Vuetify component instance produces a union too complex for TS to represent.
12
- const colorPickerRef = useTemplateRef<HTMLElement>("colorPickerRef");
13
+ const colorPickerWrapperRef = useTemplateRef<HTMLElement>("colorPickerWrapperRef");
14
+ const { pressed } = useMousePressed({ target: colorPickerWrapperRef });
15
+
13
16
  const model = defineModel<RGBAColor>();
14
- const { pressed } = useMousePressed({ target: colorPickerRef });
17
+ const { copy, copied } = useCopyToClipboard();
18
+
19
+ const currentMode = ref(disabledAlpha ? "rgb" : "rgba");
20
+ const colorInputText = ref("");
15
21
 
16
- // The model is always bound by every current caller (ColoringTypeSelector.vue, AttributeSelector.vue); defineModel can't express that as a required prop without breaking its optional v-model contract, so this reads it as defined here.
17
22
  const initialColor = model.value as RGBAColor;
18
23
  const vuetifyColor = ref({
19
24
  r: initialColor.red,
@@ -22,69 +27,246 @@ const vuetifyColor = ref({
22
27
  a: initialColor.alpha,
23
28
  });
24
29
 
30
+ function updateInputTextFromColor(red: number, green: number, blue: number, alpha: number) {
31
+ colorInputText.value =
32
+ disabledAlpha || currentMode.value === "rgb"
33
+ ? `${red}, ${green}, ${blue}`
34
+ : `${red}, ${green}, ${blue}, ${alpha}`;
35
+ }
36
+
37
+ interface VuetifyColor {
38
+ r: number;
39
+ g: number;
40
+ b: number;
41
+ a: number;
42
+ }
43
+
44
+ function commitColorToModel() {
45
+ const { r: red, g: green, b: blue, a: alpha } = vuetifyColor.value;
46
+ model.value = { red, green, blue, alpha };
47
+ }
48
+
49
+ function onPickerUpdate(color: VuetifyColor) {
50
+ const red = Math.round(color.r);
51
+ const green = Math.round(color.g);
52
+ const blue = Math.round(color.b);
53
+ const alpha = disabledAlpha ? 1 : Number(color.a.toFixed(2));
54
+
55
+ vuetifyColor.value = { r: red, g: green, b: blue, a: alpha };
56
+ updateInputTextFromColor(red, green, blue, alpha);
57
+ if (!pressed.value) {
58
+ commitColorToModel();
59
+ }
60
+ }
61
+
62
+ function toggleMode() {
63
+ if (disabledAlpha) {
64
+ return;
65
+ }
66
+ currentMode.value = currentMode.value === "rgba" ? "rgb" : "rgba";
67
+ const { r: red, g: green, b: blue, a: alpha } = vuetifyColor.value;
68
+ updateInputTextFromColor(red, green, blue, alpha);
69
+ }
70
+
71
+ async function copyToClipboard() {
72
+ const { r: red, g: green, b: blue, a: alpha } = vuetifyColor.value;
73
+ await copy(formatColorString({ red, green, blue, alpha }, currentMode.value));
74
+ }
75
+
76
+ function parseAndApplyText(text: string) {
77
+ const parsed = parseColorString(text);
78
+ if (!parsed) {
79
+ return false;
80
+ }
81
+
82
+ const { red, green, blue, alpha: rawAlpha } = parsed;
83
+ const alpha = disabledAlpha ? 1 : rawAlpha;
84
+
85
+ if (!disabledAlpha && alpha < 1) {
86
+ currentMode.value = "rgba";
87
+ }
88
+
89
+ vuetifyColor.value = { r: red, g: green, b: blue, a: alpha };
90
+ model.value = { red, green, blue, alpha };
91
+ updateInputTextFromColor(red, green, blue, alpha);
92
+ return true;
93
+ }
94
+
95
+ function onInputPaste(event: ClipboardEvent) {
96
+ event.preventDefault();
97
+ const text = event.clipboardData!.getData("text/plain");
98
+ parseAndApplyText(text);
99
+ }
100
+
101
+ function onInputCommit() {
102
+ if (!parseAndApplyText(colorInputText.value)) {
103
+ const { r: red, g: green, b: blue, a: alpha } = vuetifyColor.value;
104
+ updateInputTextFromColor(red, green, blue, alpha);
105
+ }
106
+ }
107
+
25
108
  watch(
26
109
  model,
27
110
  (newValue) => {
28
111
  if (!newValue) {
29
112
  return;
30
113
  }
31
- const hasChanged =
32
- newValue.red !== vuetifyColor.value.r ||
33
- newValue.green !== vuetifyColor.value.g ||
34
- newValue.blue !== vuetifyColor.value.b ||
35
- newValue.alpha !== vuetifyColor.value.a;
36
- if (!hasChanged) {
37
- return;
114
+ const { red, green, blue, alpha } = newValue;
115
+ if (
116
+ vuetifyColor.value.r !== red ||
117
+ vuetifyColor.value.g !== green ||
118
+ vuetifyColor.value.b !== blue ||
119
+ vuetifyColor.value.a !== alpha
120
+ ) {
121
+ vuetifyColor.value = { r: red, g: green, b: blue, a: alpha };
38
122
  }
39
- vuetifyColor.value = {
40
- r: newValue.red,
41
- g: newValue.green,
42
- b: newValue.blue,
43
- a: newValue.alpha,
44
- };
123
+ updateInputTextFromColor(red, green, blue, alpha);
45
124
  },
46
- { deep: true },
125
+ { deep: true, immediate: true },
47
126
  );
48
127
 
49
- watch(pressed, (value) => {
50
- if (!value) {
51
- model.value = {
52
- red: vuetifyColor.value.r,
53
- green: vuetifyColor.value.g,
54
- blue: vuetifyColor.value.b,
55
- alpha: vuetifyColor.value.a,
56
- };
128
+ watch(pressed, (isPressed) => {
129
+ if (!isPressed) {
130
+ commitColorToModel();
57
131
  }
58
132
  });
133
+
134
+ watch(
135
+ () => disabledAlpha,
136
+ (disabled) => {
137
+ if (disabled) {
138
+ currentMode.value = "rgb";
139
+ if (model.value) {
140
+ model.value = { ...model.value, alpha: 1 };
141
+ }
142
+ }
143
+ },
144
+ );
59
145
  </script>
60
146
 
61
147
  <template>
62
- <v-color-picker
63
- ref="colorPickerRef"
64
- data-testid="colorPicker"
65
- v-model="vuetifyColor"
66
- flat
67
- canvas-height="75"
68
- hide-inputs
69
- hide-eye-dropper
70
- :disabled-alpha="disabledAlpha"
71
- width="220"
72
- :mode="disabledAlpha ? 'rgb' : 'rgba'"
73
- class="mx-auto"
74
- />
148
+ <div
149
+ ref="colorPickerWrapperRef"
150
+ class="color-picker-wrapper mx-auto rounded-lg overflow-hidden border"
151
+ >
152
+ <v-color-picker
153
+ data-testid="colorPicker"
154
+ :model-value="vuetifyColor"
155
+ @update:model-value="onPickerUpdate"
156
+ mode="rgba"
157
+ flat
158
+ canvas-height="75"
159
+ hide-eye-dropper
160
+ hide-inputs
161
+ :disabled-alpha="disabledAlpha"
162
+ width="220"
163
+ class="mx-auto bg-transparent"
164
+ />
165
+
166
+ <div class="color-picker-controls pa-2 pt-0">
167
+ <div class="d-flex align-center justify-space-between px-1 mb-1">
168
+ <div class="d-flex align-center ga-1">
169
+ <span
170
+ class="text-caption font-weight-medium text-uppercase text-medium-emphasis"
171
+ style="font-size: 0.68rem"
172
+ >
173
+ {{ currentMode }}
174
+ </span>
175
+ <v-btn
176
+ v-if="!disabledAlpha"
177
+ icon
178
+ density="compact"
179
+ variant="text"
180
+ size="small"
181
+ @click="toggleMode"
182
+ v-tooltip="'Swap mode (RGB / RGBA)'"
183
+ >
184
+ <v-icon icon="mdi-swap-horizontal" size="16" />
185
+ </v-btn>
186
+ </div>
187
+
188
+ <v-btn
189
+ data-testid="copyColorBtn"
190
+ icon
191
+ density="compact"
192
+ variant="text"
193
+ size="small"
194
+ @click="copyToClipboard"
195
+ v-tooltip="'Copy to clipboard'"
196
+ >
197
+ <v-icon
198
+ :icon="copied ? 'mdi-check' : 'mdi-content-copy'"
199
+ size="14"
200
+ :color="copied ? 'success' : undefined"
201
+ />
202
+ </v-btn>
203
+ </div>
204
+
205
+ <input
206
+ data-testid="colorInput"
207
+ v-model="colorInputText"
208
+ class="color-input-field"
209
+ type="text"
210
+ spellcheck="false"
211
+ autocomplete="off"
212
+ @paste="onInputPaste"
213
+ @change="onInputCommit"
214
+ @keydown.enter.prevent="onInputCommit"
215
+ />
216
+ </div>
217
+ </div>
75
218
  </template>
76
219
 
77
220
  <style scoped>
221
+ .color-picker-wrapper {
222
+ width: 220px;
223
+ background-color: rgba(33, 33, 33, 0.95);
224
+ border: 1px solid rgba(255, 255, 255, 0.12) !important;
225
+ outline: none;
226
+ }
227
+
228
+ :deep(.v-color-picker) {
229
+ background-color: transparent !important;
230
+ box-shadow: none !important;
231
+ }
232
+
233
+ :deep(.v-color-picker-canvas),
234
+ :deep(.v-color-picker__preview),
235
+ :deep(.v-color-picker__controls) {
236
+ box-shadow: none !important;
237
+ }
238
+
78
239
  :deep(.v-color-picker__controls) {
79
- padding: 8px !important;
240
+ padding: 4px 8px 0 !important;
80
241
  }
81
242
 
82
243
  :deep(.v-color-picker__dot) {
83
244
  width: 18px !important;
84
245
  height: 18px !important;
246
+ box-shadow: none !important;
85
247
  }
86
248
 
87
249
  :deep(.v-color-picker__preview) {
88
250
  margin-bottom: 0 !important;
89
251
  }
252
+
253
+ .color-input-field {
254
+ width: 100%;
255
+ box-sizing: border-box;
256
+ padding: 6px 10px;
257
+ background-color: rgba(255, 255, 255, 0.08);
258
+ border: 1px solid transparent;
259
+ border-radius: 6px;
260
+ outline: none;
261
+ color: inherit;
262
+ font-size: 0.82rem;
263
+ font-family: monospace;
264
+ letter-spacing: 0.5px;
265
+ text-align: center;
266
+ transition: border-color 0.15s ease;
267
+ }
268
+
269
+ .color-input-field:focus {
270
+ border-color: rgba(255, 255, 255, 0.3);
271
+ }
90
272
  </style>
@@ -0,0 +1,7 @@
1
+ import { useClipboard } from "@vueuse/core";
2
+
3
+ const COPIED_FEEDBACK_TIMEOUT = 1500;
4
+
5
+ export function useCopyToClipboard(): ReturnType<typeof useClipboard> {
6
+ return useClipboard({ copiedDuring: COPIED_FEEDBACK_TIMEOUT });
7
+ }
package/app/stores/app.ts CHANGED
@@ -170,6 +170,7 @@ export const useAppStore = defineStore("app", () => {
170
170
 
171
171
  // `TResult` is asserted, not verified, at the single `return result as TResult` boundary below: the backend response is only checked against `schema` at runtime, so callers' `TResult` is a contract with the schema, not something this function can prove.
172
172
  // oxlint-disable-next-line unicorn/consistent-function-scoping
173
+ // oxlint-disable-next-line unicorn/consistent-function-scoping
173
174
  async function request<TResult = unknown>(
174
175
  { schema, params }: { schema: JsonRpcSchema; params?: Record<string, unknown> },
175
176
  callbacks: RequestHandlers = {},
@@ -0,0 +1,54 @@
1
+ const RGB_MAX = 255;
2
+ const PERCENT_MAX = 100;
3
+
4
+ function parseComponent(val, max = RGB_MAX) {
5
+ if (val.endsWith("%")) {
6
+ return (Number(val.slice(0, -1)) / PERCENT_MAX) * max;
7
+ }
8
+ return Number(val);
9
+ }
10
+
11
+ function parseColorString(colorText) {
12
+ if (!colorText || typeof colorText !== "string") {
13
+ return undefined;
14
+ }
15
+
16
+ const numbersMatch = colorText.match(/[\d.]+%?/gu);
17
+ if (!numbersMatch || (numbersMatch.length !== 3 && numbersMatch.length !== 4)) {
18
+ return undefined;
19
+ }
20
+
21
+ const rawRed = parseComponent(numbersMatch[0]);
22
+ const rawGreen = parseComponent(numbersMatch[1]);
23
+ const rawBlue = parseComponent(numbersMatch[2]);
24
+
25
+ const red = Math.min(RGB_MAX, Math.max(0, Math.round(rawRed)));
26
+ const green = Math.min(RGB_MAX, Math.max(0, Math.round(rawGreen)));
27
+ const blue = Math.min(RGB_MAX, Math.max(0, Math.round(rawBlue)));
28
+
29
+ let alpha = 1;
30
+ if (numbersMatch.length === 4) {
31
+ const rawAlpha = parseComponent(numbersMatch[3], 1);
32
+ const calculatedAlpha = rawAlpha > 1 ? rawAlpha / RGB_MAX : rawAlpha;
33
+ const roundedAlpha = Number(calculatedAlpha.toFixed(2));
34
+ alpha = Math.min(1, Math.max(0, roundedAlpha));
35
+ }
36
+
37
+ if (Number.isNaN(red) || Number.isNaN(green) || Number.isNaN(blue) || Number.isNaN(alpha)) {
38
+ return undefined;
39
+ }
40
+
41
+ return { red, green, blue, alpha };
42
+ }
43
+
44
+ function formatColorString(color, mode) {
45
+ const { red, green, blue, alpha } = color;
46
+ const roundRed = Math.round(red);
47
+ const roundGreen = Math.round(green);
48
+ const roundBlue = Math.round(blue);
49
+ return mode === "rgb"
50
+ ? `rgb(${roundRed}, ${roundGreen}, ${roundBlue})`
51
+ : `rgba(${roundRed}, ${roundGreen}, ${roundBlue}, ${alpha})`;
52
+ }
53
+
54
+ export { parseColorString, formatColorString };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geode/opengeodeweb-front",
3
- "version": "10.34.0-rc.14",
3
+ "version": "10.34.0-rc.15",
4
4
  "description": "OpenSource Vue/Nuxt/Pinia/Vuetify framework for web applications",
5
5
  "homepage": "https://github.com/Geode-solutions/OpenGeodeWeb-Front",
6
6
  "bugs": {