@tekkare/romulus 0.1.20 → 0.1.22

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
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.16.0"
6
6
  },
7
- "version": "0.1.19",
7
+ "version": "0.1.21",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
@@ -0,0 +1,164 @@
1
+ <!--
2
+ RmSegmented - un choix unique parmi quelques options, rendu comme un rail
3
+ segmente : les options cote a cote dans un cadre, et un pave colore qui
4
+ glisse sur celle qui est retenue.
5
+
6
+ A preferer a une rangee de boutons radio des que les options sont courtes et
7
+ peu nombreuses. Une pastille ronde par option ne dit pas qu'elles sont
8
+ exclusives : il faut le deduire de la forme du controle. Le rail, lui, montre
9
+ qu'une seule case est prise a la fois, et occupe la moitie de la hauteur.
10
+ Au-dela d'une dizaine d'options, ou si les libelles sont longs, c'est une
11
+ liste deroulante qu'il faut, pas ce composant.
12
+
13
+ Le pave est positionne par mesure du DOM, pas par calcul de fractions : les
14
+ segments n'ont pas la meme largeur (« Product » contre « Molecule (ATC5) »),
15
+ et la largeur d'un libelle depend de la police, donc du moment ou elle finit
16
+ de charger. Un ResizeObserver reprend la mesure a chaque changement de taille.
17
+ Il porte aussi `top` et `height`, pour que le pave suive quand le rail passe
18
+ sur deux lignes faute de place.
19
+
20
+ Clavier : le groupe est un `radiogroup` ARIA, les fleches deplacent la
21
+ selection, comme sur un vrai groupe de boutons radio.
22
+ -->
23
+ <script setup lang="ts">
24
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
25
+
26
+ export interface RmSegmentedOption {
27
+ /** Valeur portee par le v-model. */
28
+ value: string | number
29
+ label: string
30
+ /** Icone Phosphor, nom en kebab-case sans le prefixe `ph-`. */
31
+ icon?: string
32
+ disabled?: boolean
33
+ }
34
+
35
+ const props = withDefaults(defineProps<{
36
+ /** Une chaine vaut `{ value: <la chaine>, label: <la chaine> }`. */
37
+ options: (string | RmSegmentedOption)[]
38
+ size?: 'sm' | 'md'
39
+ /** Le rail prend toute la largeur, segments a parts egales. */
40
+ block?: boolean
41
+ ariaLabel?: string
42
+ }>(), {
43
+ size: 'md',
44
+ block: false,
45
+ ariaLabel: undefined,
46
+ })
47
+
48
+ const model = defineModel<string | number | undefined>({ default: undefined })
49
+
50
+ const items = computed<RmSegmentedOption[]>(() =>
51
+ props.options.map(option =>
52
+ typeof option === 'string' ? { value: option, label: option } : option,
53
+ ),
54
+ )
55
+
56
+ const activeIndex = computed(() => items.value.findIndex(item => item.value === model.value))
57
+
58
+ const root = ref<HTMLElement | null>(null)
59
+ const segments = ref<HTMLElement[]>([])
60
+
61
+ /**
62
+ * Le pave n'existe pas tant qu'il n'a pas ete mesure : sans ca il apparait un
63
+ * instant sur la premiere option, a la mauvaise largeur, avant de sauter a sa
64
+ * place.
65
+ */
66
+ const thumb = ref<Record<string, string> | null>(null)
67
+
68
+ function measure() {
69
+ const element = segments.value[activeIndex.value]
70
+ if (!element || !root.value) {
71
+ thumb.value = null
72
+ return
73
+ }
74
+ thumb.value = {
75
+ width: `${element.offsetWidth}px`,
76
+ height: `${element.offsetHeight}px`,
77
+ transform: `translate(${element.offsetLeft}px, ${element.offsetTop}px)`,
78
+ }
79
+ }
80
+
81
+ const remeasure = () => nextTick(measure)
82
+
83
+ function select(item: RmSegmentedOption) {
84
+ if (item.disabled || item.value === model.value) return
85
+ model.value = item.value
86
+ }
87
+
88
+ /** Fleches : on saute les options desactivees, et on boucle aux extremites. */
89
+ function move(step: number) {
90
+ const total = items.value.length
91
+ if (!total) return
92
+ let index = activeIndex.value < 0 ? 0 : activeIndex.value
93
+ for (let hop = 0; hop < total; hop++) {
94
+ index = (index + step + total) % total
95
+ const candidate = items.value[index]
96
+ if (candidate && !candidate.disabled) {
97
+ model.value = candidate.value
98
+ return
99
+ }
100
+ }
101
+ }
102
+
103
+ let observer: ResizeObserver | null = null
104
+
105
+ onMounted(() => {
106
+ remeasure()
107
+ // La police est chargee apres le premier rendu : sans cette reprise, le pave
108
+ // garde la largeur mesuree sur la police de repli.
109
+ if (typeof document !== 'undefined' && 'fonts' in document) {
110
+ (document as any).fonts?.ready?.then(remeasure).catch(() => {})
111
+ }
112
+ if (root.value && typeof ResizeObserver !== 'undefined') {
113
+ observer = new ResizeObserver(measure)
114
+ observer.observe(root.value)
115
+ }
116
+ })
117
+
118
+ onBeforeUnmount(() => {
119
+ observer?.disconnect()
120
+ observer = null
121
+ })
122
+
123
+ watch(() => [model.value, items.value.length, props.block], remeasure)
124
+ </script>
125
+
126
+ <template>
127
+ <div
128
+ ref="root"
129
+ class="rm-seg"
130
+ :class="[`rm-seg--${size}`, { 'rm-seg--block': block }]"
131
+ role="radiogroup"
132
+ :aria-label="ariaLabel"
133
+ @keydown.left.prevent="move(-1)"
134
+ @keydown.up.prevent="move(-1)"
135
+ @keydown.right.prevent="move(1)"
136
+ @keydown.down.prevent="move(1)"
137
+ >
138
+ <span v-if="thumb" class="rm-seg__thumb" :style="thumb" aria-hidden="true" />
139
+
140
+ <button
141
+ v-for="(item, index) in items"
142
+ :key="item.value"
143
+ ref="segments"
144
+ class="rm-seg__option"
145
+ :class="{
146
+ 'rm-seg__option--active': index === activeIndex,
147
+ 'rm-seg__option--sep': index > 0 && index !== activeIndex && index - 1 !== activeIndex,
148
+ }"
149
+ type="button"
150
+ role="radio"
151
+ :aria-checked="index === activeIndex"
152
+ :disabled="item.disabled"
153
+ :tabindex="index === activeIndex || (activeIndex < 0 && index === 0) ? 0 : -1"
154
+ @click="select(item)"
155
+ >
156
+ <i v-if="item.icon" :class="`ph ph-${item.icon}`" />
157
+ {{ item.label }}
158
+ </button>
159
+ </div>
160
+ </template>
161
+
162
+ <style scoped>
163
+ .rm-seg{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-lg);display:inline-flex;flex-wrap:wrap;font-family:var(--font-sans);gap:2px;max-width:100%;padding:3px;position:relative}.rm-seg--block{display:flex;width:100%}.rm-seg--block .rm-seg__option{flex:1}.rm-seg__thumb{background:var(--color-primary);border-radius:var(--radius-md);left:0;position:absolute;top:0;transition:transform var(--duration-base) var(--ease-in-out),width var(--duration-base) var(--ease-in-out),height var(--duration-base) var(--ease-in-out);z-index:0}.rm-seg__option{align-items:center;-moz-appearance:none;appearance:none;-webkit-appearance:none;background:none;border:none;border-radius:var(--radius-md);box-shadow:none;color:var(--text-secondary);cursor:pointer;display:inline-flex;font-family:inherit;font-weight:500;gap:6px;justify-content:center;margin:0;position:relative;transition:color var(--duration-fast) var(--ease-in-out);white-space:nowrap;z-index:1}.rm-seg--md .rm-seg__option{font-size:14px;line-height:20px;padding:5px 12px}.rm-seg--sm .rm-seg__option{font-size:13px;line-height:18px;padding:3px 10px}.rm-seg__option i{font-size:16px;line-height:1}.rm-seg__option:hover:not(:disabled):not(.rm-seg__option--active){color:var(--color-primary)}.rm-seg__option--active{color:#fff;font-weight:600}.rm-seg__option:disabled{color:var(--text-muted);cursor:not-allowed;opacity:.6}.rm-seg__option:focus-visible{outline:2px solid var(--color-primary);outline-offset:1px}.rm-seg__option--sep:before{background:var(--border-default);content:"";height:60%;left:-2px;position:absolute;top:20%;width:1px}@media (prefers-reduced-motion:reduce){.rm-seg__thumb{transition:none}}
164
+ </style>
@@ -24,6 +24,40 @@ const star = (cx, cy, r, fill = "#FFFFFF") => {
24
24
  });
25
25
  return `<polygon points="${points.join(" ")}" fill="${fill}"/>`;
26
26
  };
27
+ const trigram = (cx, cy, angle) => `
28
+ <g transform="translate(${cx} ${cy}) rotate(${angle})" fill="#000000">
29
+ <rect x="-1.5" y="-1.15" width="3" height="0.5"/>
30
+ <rect x="-1.5" y="-0.25" width="3" height="0.5"/>
31
+ <rect x="-1.5" y="0.65" width="3" height="0.5"/>
32
+ </g>`;
33
+ const MAPLE = (() => {
34
+ const outline = [
35
+ [50, 4],
36
+ [54, 24],
37
+ [68, 20],
38
+ [65, 32],
39
+ [80, 29],
40
+ [76, 41],
41
+ [96, 45],
42
+ [78, 57],
43
+ [84, 70],
44
+ [64, 66],
45
+ [62, 78],
46
+ [50, 62],
47
+ [38, 78],
48
+ [36, 66],
49
+ [16, 70],
50
+ [22, 57],
51
+ [4, 45],
52
+ [24, 41],
53
+ [20, 29],
54
+ [35, 32],
55
+ [32, 20],
56
+ [46, 24]
57
+ ];
58
+ const points = outline.map(([x, y]) => `${(10.5 + (x - 50) * 0.088).toFixed(2)},${(7.5 + (y - 41) * 0.108).toFixed(2)}`).join(" ");
59
+ return `<polygon points="${points}" fill="#D80621"/>`;
60
+ })();
27
61
  export const RM_FLAGS = {
28
62
  fr: vertical("#002654", "#FFFFFF", "#ED2939"),
29
63
  be: vertical("#000000", "#FDDA24", "#EF3340"),
@@ -51,15 +85,14 @@ export const RM_FLAGS = {
51
85
  ${[1, 3, 5, 7, 9, 11].map((i) => `<rect y="${(i * 15 / 13).toFixed(2)}" width="21" height="${(15 / 13).toFixed(2)}" fill="#FFFFFF"/>`).join("")}
52
86
  <rect width="8.4" height="${(7 * 15 / 13).toFixed(2)}" fill="#0A3161"/>
53
87
  ${[0, 1, 2].flatMap((row) => [0, 1, 2].map((col) => `<circle cx="${2 + col * 2.2}" cy="${1.8 + row * 2.4}" r="0.5" fill="#FFFFFF"/>`)).join("")}`,
54
- ca: `<rect width="21" height="15" fill="#FFFFFF"/><rect width="5.25" height="15" fill="#D80621"/><rect x="15.75" width="5.25" height="15" fill="#D80621"/>
55
- <path d="M10.5 3.2 11.3 5.2 12.9 4.6 12.4 6.6 14 6.3 13.3 7.6 14.6 8.6 12.9 9.2 13.2 10.3 11.2 10 11.4 12.3 10.5 11.4 9.6 12.3 9.8 10 7.8 10.3 8.1 9.2 6.4 8.6 7.7 7.6 7 6.3 8.6 6.6 8.1 4.6 9.7 5.2Z" fill="#D80621"/>`,
88
+ ca: `<rect width="21" height="15" fill="#FFFFFF"/><rect width="5.25" height="15" fill="#D80621"/><rect x="15.75" width="5.25" height="15" fill="#D80621"/>${MAPLE}`,
56
89
  jp: `<rect width="21" height="15" fill="#FFFFFF"/><circle cx="10.5" cy="7.5" r="4.2" fill="#BC002D"/>`,
57
90
  // Taegeuk simplifie : deux demi-disques et les quatre trigrammes ramenes a
58
91
  // des barres. A 21x15 les traits interrompus ne se distinguent plus.
59
92
  kr: `<rect width="21" height="15" fill="#FFFFFF"/>
60
- <path d="M7.2 7.5a3.3 3.3 0 0 1 6.6 0 1.65 1.65 0 0 0-3.3 0 1.65 1.65 0 0 1-3.3 0Z" fill="#CD2E3A"/>
61
- <path d="M7.2 7.5a1.65 1.65 0 0 1 3.3 0 1.65 1.65 0 0 0 3.3 0 3.3 3.3 0 0 1-6.6 0Z" fill="#0047A0"/>
62
- <g stroke="#000000" stroke-width="0.7"><path d="M2.6 3.1 4.8 4.6M2 4 4.2 5.5M4.2 10.4 2 11.9M4.8 11.3 2.6 12.8M16.2 4.6 18.4 3.1M16.8 5.5 19 4M16.2 10.4 18.4 11.9M16.8 11.3 19 12.8"/></g>`,
93
+ <path d="M6.9 7.5a3.6 3.6 0 0 1 7.2 0 1.8 1.8 0 0 0-3.6 0 1.8 1.8 0 0 1-3.6 0Z" fill="#CD2E3A"/>
94
+ <path d="M6.9 7.5a1.8 1.8 0 0 1 3.6 0 1.8 1.8 0 0 0 3.6 0 3.6 3.6 0 0 1-7.2 0Z" fill="#0047A0"/>
95
+ ${trigram(3.4, 3.2, -58.4)}${trigram(17.6, 3.2, 58.4)}${trigram(3.4, 11.8, 58.4)}${trigram(17.6, 11.8, -58.4)}`,
63
96
  cn: `<rect width="21" height="15" fill="#EE1C25"/>${star(4.5, 4.2, 2.4, "#FFDE00")}${star(8.6, 1.9, 0.9, "#FFDE00")}${star(9.9, 3.7, 0.9, "#FFDE00")}${star(9.9, 5.9, 0.9, "#FFDE00")}${star(8.6, 7.6, 0.9, "#FFDE00")}`,
64
97
  au: `<rect width="21" height="15" fill="#012169"/><g transform="scale(0.5)">${UNION}</g>
65
98
  ${star(5.2, 11.4, 1.1)}${star(16.5, 3.4, 0.9)}${star(18.2, 7.4, 0.9)}${star(16.2, 11.2, 0.9)}${star(14.2, 8.6, 0.6)}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekkare/romulus",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",