@tekkare/romulus 0.1.77 → 0.1.79
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.
package/dist/module.json
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
-->
|
|
28
28
|
<script setup lang="ts">
|
|
29
29
|
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
30
|
+
import { PhCaretDown, PhInfo, PhMagnifyingGlass, PhMinusCircle, PhPlusCircle } from '@phosphor-icons/vue'
|
|
30
31
|
import { rmPanelOpened, rmPanelRegister } from '../utils/panels'
|
|
31
32
|
|
|
32
33
|
export interface RmComplexSelectPreset {
|
|
@@ -133,9 +134,24 @@ const isOpen = ref(false)
|
|
|
133
134
|
const addSearch = ref('')
|
|
134
135
|
const removeSearch = ref('')
|
|
135
136
|
const activePreset = ref<string>()
|
|
137
|
+
const alignRight = ref(false)
|
|
136
138
|
const rootRef = ref<HTMLElement>()
|
|
137
139
|
const addRef = ref<HTMLInputElement>()
|
|
138
140
|
|
|
141
|
+
/** Largeur du panneau, tenue en phase avec la regle `.rm-csel__panel`. */
|
|
142
|
+
const PANEL_WIDTH = 820
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Le panneau sort a gauche du champ par defaut. Pose sur un filtre de bout de
|
|
146
|
+
* rangee, il depasserait de l'ecran : il se cale alors sur le bord droit du
|
|
147
|
+
* champ.
|
|
148
|
+
*/
|
|
149
|
+
function placePanel() {
|
|
150
|
+
const rect = rootRef.value?.getBoundingClientRect()
|
|
151
|
+
if (!rect) return
|
|
152
|
+
alignRight.value = rect.left + PANEL_WIDTH > window.innerWidth - 8
|
|
153
|
+
}
|
|
154
|
+
|
|
139
155
|
/** La colonne de gauche ne propose que ce qui n'est pas deja retenu. */
|
|
140
156
|
const addOptions = computed(() => {
|
|
141
157
|
const needle = addSearch.value.trim().toLowerCase()
|
|
@@ -180,6 +196,7 @@ async function toggle() {
|
|
|
180
196
|
if (isOpen.value) return close()
|
|
181
197
|
|
|
182
198
|
isOpen.value = true
|
|
199
|
+
placePanel()
|
|
183
200
|
rmPanelOpened(close)
|
|
184
201
|
document.addEventListener('click', onDocumentClick)
|
|
185
202
|
await nextTick()
|
|
@@ -238,7 +255,7 @@ onUnmounted(() => {
|
|
|
238
255
|
<p class="rm-csel__label">
|
|
239
256
|
{{ label }}
|
|
240
257
|
<button v-if="labelInfo" type="button" class="rm-csel__info" @click="emit('openInfo')">
|
|
241
|
-
<
|
|
258
|
+
<PhInfo :size="14" />
|
|
242
259
|
</button>
|
|
243
260
|
</p>
|
|
244
261
|
|
|
@@ -259,10 +276,15 @@ onUnmounted(() => {
|
|
|
259
276
|
<span v-if="showAmount && selected.length > 0" class="rm-csel__count">
|
|
260
277
|
{{ selected.length > 99 ? '99+' : selected.length }}
|
|
261
278
|
</span>
|
|
262
|
-
<
|
|
279
|
+
<PhCaretDown :size="16" />
|
|
263
280
|
</button>
|
|
264
281
|
|
|
265
|
-
<div
|
|
282
|
+
<div
|
|
283
|
+
v-show="isOpen"
|
|
284
|
+
class="rm-csel__panel"
|
|
285
|
+
:class="{ 'rm-csel__panel--right': alignRight }"
|
|
286
|
+
@click.stop
|
|
287
|
+
>
|
|
266
288
|
<div class="rm-csel__column">
|
|
267
289
|
<div v-if="presets && presets.length > 0" class="rm-csel__presets">
|
|
268
290
|
<h2 class="rm-csel__section">{{ t.quick }}</h2>
|
|
@@ -295,7 +317,7 @@ onUnmounted(() => {
|
|
|
295
317
|
</div>
|
|
296
318
|
|
|
297
319
|
<div class="rm-csel__search">
|
|
298
|
-
<
|
|
320
|
+
<PhMagnifyingGlass :size="14" />
|
|
299
321
|
<input
|
|
300
322
|
ref="addRef"
|
|
301
323
|
v-model="addSearch"
|
|
@@ -321,7 +343,7 @@ onUnmounted(() => {
|
|
|
321
343
|
class="rm-csel__option rm-csel__option--add"
|
|
322
344
|
@click="add(option)"
|
|
323
345
|
>
|
|
324
|
-
<
|
|
346
|
+
<PhPlusCircle :size="16" weight="fill" />
|
|
325
347
|
<span>{{ option }}</span>
|
|
326
348
|
</button>
|
|
327
349
|
</div>
|
|
@@ -341,7 +363,7 @@ onUnmounted(() => {
|
|
|
341
363
|
</div>
|
|
342
364
|
|
|
343
365
|
<div class="rm-csel__search">
|
|
344
|
-
<
|
|
366
|
+
<PhMagnifyingGlass :size="14" />
|
|
345
367
|
<input
|
|
346
368
|
v-model="removeSearch"
|
|
347
369
|
type="search"
|
|
@@ -363,7 +385,7 @@ onUnmounted(() => {
|
|
|
363
385
|
class="rm-csel__option rm-csel__option--remove"
|
|
364
386
|
@click="remove(option)"
|
|
365
387
|
>
|
|
366
|
-
<
|
|
388
|
+
<PhMinusCircle :size="16" weight="fill" />
|
|
367
389
|
<span>{{ option }}</span>
|
|
368
390
|
</button>
|
|
369
391
|
</div>
|
|
@@ -373,5 +395,5 @@ onUnmounted(() => {
|
|
|
373
395
|
</template>
|
|
374
396
|
|
|
375
397
|
<style scoped>
|
|
376
|
-
.rm-csel{align-items:flex-start;display:flex;flex-direction:column;gap:4px;position:relative}.rm-csel__label{align-items:center;color:var(--text-secondary);display:flex;font-size:var(--font-12,12px);gap:4px;line-height:14px}.rm-csel__info{background:none;border:none;color:var(--text-muted);cursor:pointer;display:flex;padding:0}.rm-csel__field{align-items:center;background:var(--bg-surface);border:1.5px solid var(--border-default);border-radius:var(--radius-md,8px);color:var(--text-primary);cursor:pointer;display:flex;font-family:inherit;font-size:var(--font-14,14px);font-weight:700;gap:4px;line-height:16px;padding:12px;text-align:left;width:196px}.rm-csel__field:hover:not(.is-disabled){border-color:rgba(var(--brand-rgb),.3)}.rm-csel__field.is-open{border-color:var(--color-primary)}.rm-csel__field.is-disabled{background:var(--bg-surface-2);color:var(--text-muted);cursor:not-allowed}.rm-csel__field:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}.rm-csel__value-slot{display:block;flex:1;min-width:0}.rm-csel__value{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rm-csel__value.is-empty{color:var(--text-muted);font-style:italic;font-weight:400}.rm-csel__field :deep(svg){color:var(--text-secondary);flex-shrink:0}.rm-csel__count{align-items:center;background:rgba(var(--brand-rgb),.1);border-radius:50%;color:var(--color-primary);display:flex;flex-shrink:0;font-size:var(--font-12,12px);font-weight:600;height:20px;justify-content:center;width:20px}.rm-csel__panel{background:var(--bg-surface);border:1.5px solid var(--border-default);border-radius:var(--radius-lg,12px);box-shadow:var(--r-shadow-lg,0 8px 24px rgba(0,0,0,.12));box-sizing:border-box;display:flex;left:0;margin-top:8px;max-width:calc(100vw - 32px);position:absolute;top:100%;width:
|
|
398
|
+
.rm-csel{align-items:flex-start;display:flex;flex-direction:column;gap:4px;position:relative}.rm-csel__label{align-items:center;color:var(--text-secondary);display:flex;font-size:var(--font-12,12px);gap:4px;line-height:14px}.rm-csel__info{background:none;border:none;color:var(--text-muted);cursor:pointer;display:flex;padding:0}.rm-csel__field{align-items:center;background:var(--bg-surface);border:1.5px solid var(--border-default);border-radius:var(--radius-md,8px);color:var(--text-primary);cursor:pointer;display:flex;font-family:inherit;font-size:var(--font-14,14px);font-weight:700;gap:4px;line-height:16px;padding:12px;text-align:left;width:196px}.rm-csel__field:hover:not(.is-disabled){border-color:rgba(var(--brand-rgb),.3)}.rm-csel__field.is-open{border-color:var(--color-primary)}.rm-csel__field.is-disabled{background:var(--bg-surface-2);color:var(--text-muted);cursor:not-allowed}.rm-csel__field:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}.rm-csel__value-slot{display:block;flex:1;min-width:0}.rm-csel__value{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rm-csel__value.is-empty{color:var(--text-muted);font-style:italic;font-weight:400}.rm-csel__field :deep(svg){color:var(--text-secondary);flex-shrink:0}.rm-csel__count{align-items:center;background:rgba(var(--brand-rgb),.1);border-radius:50%;color:var(--color-primary);display:flex;flex-shrink:0;font-size:var(--font-12,12px);font-weight:600;height:20px;justify-content:center;width:20px}.rm-csel__panel{background:var(--bg-surface);border:1.5px solid var(--border-default);border-radius:var(--radius-lg,12px);box-shadow:var(--r-shadow-lg,0 8px 24px rgba(0,0,0,.12));box-sizing:border-box;display:flex;left:0;margin-top:8px;max-width:calc(100vw - 32px);position:absolute;right:auto;top:100%;width:820px;z-index:12}.rm-csel__panel--right{left:auto;right:0}.rm-csel__column{display:flex;flex:1;flex-direction:column;gap:8px;min-width:0;padding:16px}.rm-csel__column--right{border-left:1px solid var(--border-soft);flex:0 0 300px}.rm-csel__presets{border-bottom:1px solid var(--border-soft);display:flex;flex-direction:column;gap:8px;padding-bottom:8px}.rm-csel__pills{display:flex;flex-wrap:wrap;gap:4px}.rm-csel__pill{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:9999px;color:var(--text-secondary);cursor:pointer;font-family:inherit;font-size:var(--font-12,12px);padding:4px 10px}.rm-csel__pill.is-active,.rm-csel__pill:hover{border-color:var(--color-primary);color:var(--color-primary)}.rm-csel__pill.is-active{background:rgba(var(--brand-rgb),.1);font-weight:700}.rm-csel__head{align-items:center;display:flex;gap:8px;justify-content:space-between}.rm-csel__section{color:var(--text-secondary);font-size:var(--font-12,12px);font-weight:700;margin:0}.rm-csel__action{background:none;border:none;color:var(--color-primary);cursor:pointer;font-family:inherit;font-size:var(--font-12,12px);font-weight:600;padding:0}.rm-csel__action:disabled{color:var(--text-muted);cursor:not-allowed}.rm-csel__action:hover:not(:disabled){text-decoration:underline}.rm-csel__action--clear:not(:disabled){color:var(--color-danger)}.rm-csel__search{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md,8px);display:flex;gap:8px;padding:6px 10px}.rm-csel__search :deep(svg){color:var(--text-muted);flex-shrink:0}.rm-csel__search-input{background:none;border:none;color:var(--text-primary);font-family:inherit;font-size:var(--font-13,13px);width:100%}.rm-csel__search-input::-moz-placeholder{color:var(--text-muted);font-size:var(--font-12,12px)}.rm-csel__search-input::placeholder{color:var(--text-muted);font-size:var(--font-12,12px)}.rm-csel__search-input:focus-visible{outline:none}.rm-csel__state{color:var(--text-secondary);font-size:var(--font-13,13px);font-style:italic;padding-top:8px}.rm-csel__loading{display:flex;flex-direction:column;gap:10px;padding-top:8px}.rm-csel__sr{height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;clip:rect(0,0,0,0);border:0;white-space:nowrap}.rm-csel__list{display:flex;flex-direction:column;gap:4px;height:260px;overflow-x:hidden;overflow-y:auto;scrollbar-width:thin}.rm-csel__option{align-items:flex-start;background:none;border:none;border-radius:var(--radius-sm,4px);color:var(--text-primary);cursor:pointer;display:flex;font-family:inherit;font-size:var(--font-13,13px);gap:8px;line-height:150%;min-width:0;padding:2px 4px;text-align:left}.rm-csel__option span{min-width:0;overflow-wrap:anywhere}.rm-csel__option :deep(svg){background:var(--bg-surface);border-radius:50%;flex-shrink:0;margin-top:3px}.rm-csel__option:hover{background:rgba(var(--brand-rgb),.05)}.rm-csel__option:focus-visible{outline:2px solid var(--color-primary);outline-offset:-2px}.rm-csel__option--add :deep(svg){color:var(--color-primary)}.rm-csel__option--remove :deep(svg){color:var(--color-danger)}@media (max-width:767px){.rm-csel__panel{flex-direction:column;width:calc(100vw - 32px)}.rm-csel__column--right{border-left:none;border-top:1px solid var(--border-soft)}.rm-csel__list{height:180px}}
|
|
377
399
|
</style>
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
-->
|
|
28
28
|
<script setup lang="ts">
|
|
29
29
|
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
30
|
+
import { PhCaretDown, PhCaretUpDown, PhCheckCircle, PhCircle, PhMagnifyingGlass } from '@phosphor-icons/vue'
|
|
30
31
|
import { rmPanelOpened, rmPanelRegister } from '../utils/panels'
|
|
31
32
|
|
|
32
33
|
const props = withDefaults(defineProps<{
|
|
@@ -187,12 +188,12 @@ onUnmounted(() => {
|
|
|
187
188
|
{{ selected || emptyLabel || t.empty }}
|
|
188
189
|
</span>
|
|
189
190
|
</RmTooltip>
|
|
190
|
-
<
|
|
191
|
+
<component :is="sortIcon ? PhCaretUpDown : PhCaretDown" :size="16" />
|
|
191
192
|
</button>
|
|
192
193
|
|
|
193
194
|
<div v-show="isOpen" class="rm-dsel__panel" @click.stop>
|
|
194
195
|
<div v-if="search" class="rm-dsel__search">
|
|
195
|
-
<
|
|
196
|
+
<PhMagnifyingGlass :size="16" />
|
|
196
197
|
<input
|
|
197
198
|
ref="searchRef"
|
|
198
199
|
v-model="searchTerm"
|
|
@@ -222,8 +223,8 @@ onUnmounted(() => {
|
|
|
222
223
|
@click="pick(option)"
|
|
223
224
|
@mousemove="activeIndex = index"
|
|
224
225
|
>
|
|
225
|
-
<
|
|
226
|
-
:
|
|
226
|
+
<component
|
|
227
|
+
:is="option === selected ? PhCheckCircle : PhCircle"
|
|
227
228
|
:size="16"
|
|
228
229
|
:weight="option === selected ? 'fill' : 'regular'"
|
|
229
230
|
/>
|
|
@@ -1,14 +1,24 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmIcon - une icone Phosphor designee par son nom.
|
|
3
|
+
|
|
4
|
+
Le composant charge le paquet d'icones a la demande : l'embarquer en entier
|
|
5
|
+
dans le premier rendu pesait plus lourd que tout le reste du panneau.
|
|
6
|
+
|
|
7
|
+
Les composants resolus sont gardes dans un cache commun a la page. Sans lui,
|
|
8
|
+
chaque instance fabriquait son propre composant asynchrone, meme pour une
|
|
9
|
+
icone deja chargee : une liste de trois cents lignes posait trois cents
|
|
10
|
+
attentes, et les icones apparaissaient apres le texte.
|
|
11
|
+
-->
|
|
1
12
|
<script setup lang="ts">
|
|
2
|
-
import { computed, defineAsyncComponent, type Component } from 'vue'
|
|
13
|
+
import { computed, defineAsyncComponent, shallowRef, type Component } from 'vue'
|
|
3
14
|
|
|
4
15
|
export interface RmIconProps {
|
|
5
|
-
/** Phosphor
|
|
16
|
+
/** Nom Phosphor sans le prefixe « Ph » (« House », « Gear », « ChartBar »). */
|
|
6
17
|
name: string
|
|
7
|
-
/**
|
|
18
|
+
/** Taille en pixels. */
|
|
8
19
|
size?: number
|
|
9
|
-
/**
|
|
20
|
+
/** Graisse du trait. `fill` donne la forme pleine. */
|
|
10
21
|
weight?: 'thin' | 'light' | 'regular' | 'bold' | 'fill' | 'duotone'
|
|
11
|
-
/** Icon color */
|
|
12
22
|
color?: string
|
|
13
23
|
}
|
|
14
24
|
|
|
@@ -18,14 +28,36 @@ const props = withDefaults(defineProps<RmIconProps>(), {
|
|
|
18
28
|
color: undefined,
|
|
19
29
|
})
|
|
20
30
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Un composant asynchrone par nom d'icone, partage par toutes les instances.
|
|
33
|
+
* Une fois le paquet charge, le composant garde sa resolution et se pose sans
|
|
34
|
+
* attente au montage suivant.
|
|
35
|
+
*/
|
|
36
|
+
const cache = new Map<string, Component>()
|
|
37
|
+
|
|
38
|
+
/** Le module, demande une seule fois quel que soit le nombre d'icones. */
|
|
39
|
+
const iconModule = shallowRef<Promise<Record<string, Component>>>()
|
|
40
|
+
|
|
41
|
+
function loadIcons() {
|
|
42
|
+
if (!iconModule.value) {
|
|
43
|
+
iconModule.value = import('@phosphor-icons/vue') as unknown as Promise<Record<string, Component>>
|
|
44
|
+
}
|
|
45
|
+
return iconModule.value
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolve(name: string) {
|
|
49
|
+
const key = `Ph${name}`
|
|
50
|
+
let component = cache.get(key)
|
|
51
|
+
if (!component) {
|
|
52
|
+
component = defineAsyncComponent(() =>
|
|
53
|
+
loadIcons().then(mod => mod[key] || mod.PhQuestion),
|
|
54
|
+
)
|
|
55
|
+
cache.set(key, component)
|
|
56
|
+
}
|
|
57
|
+
return component
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const iconComponent = computed(() => resolve(props.name))
|
|
29
61
|
</script>
|
|
30
62
|
|
|
31
63
|
<template>
|
|
@@ -36,6 +68,8 @@ const iconComponent = computed<Component>(() => {
|
|
|
36
68
|
:weight="weight"
|
|
37
69
|
:color="color"
|
|
38
70
|
/>
|
|
71
|
+
<!-- La place est reservee des le premier rendu : sans elle, le texte se
|
|
72
|
+
decale lateralement quand l'icone arrive. -->
|
|
39
73
|
<template #fallback>
|
|
40
74
|
<span :style="{ display: 'inline-block', width: size + 'px', height: size + 'px' }" />
|
|
41
75
|
</template>
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
-->
|
|
26
26
|
<script setup lang="ts">
|
|
27
27
|
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
28
|
+
import { PhCaretDown, PhCheckSquare, PhInfo, PhMagnifyingGlass, PhSquare } from '@phosphor-icons/vue'
|
|
28
29
|
import { rmPanelOpened, rmPanelRegister } from '../utils/panels'
|
|
29
30
|
|
|
30
31
|
const props = withDefaults(defineProps<{
|
|
@@ -175,7 +176,7 @@ onUnmounted(() => {
|
|
|
175
176
|
<p class="rm-msel__label">
|
|
176
177
|
{{ label }}
|
|
177
178
|
<button v-if="labelInfo" type="button" class="rm-msel__info" @click="emit('openInfo')">
|
|
178
|
-
<
|
|
179
|
+
<PhInfo :size="14" />
|
|
179
180
|
</button>
|
|
180
181
|
</p>
|
|
181
182
|
|
|
@@ -196,12 +197,12 @@ onUnmounted(() => {
|
|
|
196
197
|
<span v-if="showAmount && selected.length > 0" class="rm-msel__count">
|
|
197
198
|
{{ selected.length > 99 ? '99+' : selected.length }}
|
|
198
199
|
</span>
|
|
199
|
-
<
|
|
200
|
+
<PhCaretDown :size="16" />
|
|
200
201
|
</button>
|
|
201
202
|
|
|
202
203
|
<div v-show="isOpen" class="rm-msel__panel" @click.stop>
|
|
203
204
|
<div v-if="search" class="rm-msel__search">
|
|
204
|
-
<
|
|
205
|
+
<PhMagnifyingGlass :size="16" />
|
|
205
206
|
<input
|
|
206
207
|
ref="searchRef"
|
|
207
208
|
v-model="searchTerm"
|
|
@@ -230,8 +231,8 @@ onUnmounted(() => {
|
|
|
230
231
|
:aria-checked="allSelected"
|
|
231
232
|
@click="toggleAll"
|
|
232
233
|
>
|
|
233
|
-
<
|
|
234
|
-
:
|
|
234
|
+
<component
|
|
235
|
+
:is="allSelected ? PhCheckSquare : PhSquare"
|
|
235
236
|
:size="16"
|
|
236
237
|
:weight="allSelected ? 'fill' : 'regular'"
|
|
237
238
|
/>
|
|
@@ -249,8 +250,8 @@ onUnmounted(() => {
|
|
|
249
250
|
:aria-checked="selected.includes(option)"
|
|
250
251
|
@click="toggleOption(option)"
|
|
251
252
|
>
|
|
252
|
-
<
|
|
253
|
-
:
|
|
253
|
+
<component
|
|
254
|
+
:is="selected.includes(option) ? PhCheckSquare : PhSquare"
|
|
254
255
|
:size="16"
|
|
255
256
|
:weight="selected.includes(option) ? 'fill' : 'regular'"
|
|
256
257
|
/>
|