@tekkare/romulus 0.1.62 → 0.1.64

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.61",
7
+ "version": "0.1.63",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
@@ -10,7 +10,8 @@
10
10
 
11
11
  Slots : #CardTitle (libelle de la puce si `customTitle`), #Rail (sur la
12
12
  barre, a cote de la puce : le reglage qu'on change tout le temps et qui ne
13
- merite pas d'ouvrir le panneau), #Search, #Filters.
13
+ merite pas d'ouvrir le panneau ; il passe sur sa propre rangee quand la page
14
+ fournit aussi #Search), #Search, #Filters.
14
15
  Le contenu projete vient encore, dans les apps, des champs Auriga
15
16
  (arg-advanced-search-bar) : les regles `:deep` en bas les remettent a la
16
17
  hauteur et au rayon de la rangee. Elles tomberont avec ces champs.
@@ -73,6 +74,14 @@ const slots = useSlots()
73
74
  const hasAdvanced = computed(() => !!slots.Advanced)
74
75
  const hasCustomize = computed(() => !!slots.Customize)
75
76
  const hasRail = computed(() => !!slots.Rail)
77
+ const hasSearch = computed(() => !!slots.Search)
78
+ /**
79
+ * Le rail ne descend sur sa propre rangee que si la barre porte AUSSI un champ
80
+ * de recherche : c'est le champ qui lui dispute la place, pas les puces. Sans
81
+ * champ, il tient a cote de « Filtres » et une rangee de plus ne ferait
82
+ * qu'eloigner le contenu du bandeau.
83
+ */
84
+ const railBelow = computed(() => hasRail.value && hasSearch.value)
76
85
 
77
86
  function toggle(which: RmFilterBarPanel) {
78
87
  panel.value = panel.value === which ? null : which
@@ -178,6 +187,12 @@ onBeforeUnmount(() => {
178
187
  <span>{{ t.customize }}</span>
179
188
  </button>
180
189
 
190
+ <!-- Rail pose sur la rangee des puces : cas ordinaire, une page sans
191
+ champ de recherche. -->
192
+ <div v-if="hasRail && !railBelow" class="rm-fbar__rail">
193
+ <slot name="Rail" />
194
+ </div>
195
+
181
196
  <div class="rm-fbar__search">
182
197
  <slot name="Search" />
183
198
 
@@ -202,13 +217,13 @@ onBeforeUnmount(() => {
202
217
  A reserver a ce qu'on change a chaque lecture ; tout le reste va
203
218
  dans le panneau, sinon la barre redevient un panneau ouvert.
204
219
 
205
- Sa propre rangee, sous les puces. Il partageait la premiere avec le
206
- champ de recherche, et les deux se disputaient la place : un rail de
207
- huit segments (les niveaux d'agregation, du produit au fabricant)
208
- sortait du cadre par la droite, ses derniers segments coupes, et le
209
- champ tombait a sa largeur minimale. Chacun sur sa ligne prend toute
210
- la largeur de la colonne. -->
211
- <div v-if="hasRail" class="rm-fbar__rail-row">
220
+ Sa propre rangee UNIQUEMENT face a un champ de recherche : les deux
221
+ se disputaient la premiere, un rail de huit segments (les niveaux
222
+ d'agregation, du produit au fabricant) sortait du cadre par la droite,
223
+ ses derniers segments coupes, et le champ tombait a sa largeur
224
+ minimale. Chacun sur sa ligne prend alors toute la largeur de la
225
+ colonne. -->
226
+ <div v-if="railBelow" class="rm-fbar__rail-row">
212
227
  <div class="rm-fbar__rail">
213
228
  <slot name="Rail" />
214
229
  </div>
@@ -78,6 +78,12 @@ export interface RmTableProps {
78
78
  groupLabel?: (value: any) => string
79
79
  /** Chaque ligne se deplie sur un detail, rendu par le slot `expanded`. */
80
80
  expandable?: boolean
81
+ /**
82
+ * Lignes par page. A zero, la table rend tout : c'est le bon choix tant
83
+ * qu'on tient sous la centaine de lignes, la molette y va plus vite qu'un
84
+ * clic. Au-dela, paginer evite de poser des milliers de `<tr>` dans le DOM.
85
+ */
86
+ pageSize?: number
81
87
  }
82
88
 
83
89
  const props = withDefaults(defineProps<RmTableProps>(), {
@@ -97,6 +103,7 @@ const props = withDefaults(defineProps<RmTableProps>(), {
97
103
  groupBy: '',
98
104
  groupLabel: undefined,
99
105
  expandable: false,
106
+ pageSize: 0,
100
107
  })
101
108
 
102
109
  const emit = defineEmits<{
@@ -104,6 +111,9 @@ const emit = defineEmits<{
104
111
  rowClick: [row: Record<string, any>, index: number]
105
112
  }>()
106
113
 
114
+ /** Page courante, a partir de 1. Exposee pour qu'une page puisse la rejouer. */
115
+ const page = defineModel<number>('page', { default: 1 })
116
+
107
117
  /** Les lignes retenues, par leur cle. */
108
118
  const selected = defineModel<(string | number)[]>('selected', { default: () => [] })
109
119
 
@@ -158,7 +168,10 @@ const sortedRows = computed(() => {
158
168
  * intitule : le rendu reste une suite de `<tr>` dans un seul `<tbody>`.
159
169
  */
160
170
  const sections = computed(() => {
161
- const rows = sortedRows.value.map((row, index) => ({ row, index }))
171
+ // L'index reste celui de la liste entiere : un slot ou un `rowClick` doit
172
+ // designer la meme ligne, quelle que soit la page ouverte.
173
+ const offset = props.pageSize > 0 ? (currentPage.value - 1) * props.pageSize : 0
174
+ const rows = pagedRows.value.map((row, i) => ({ row, index: offset + i }))
162
175
  if (!props.groupBy) return [{ label: '', rows }]
163
176
 
164
177
  const buckets = new Map<any, typeof rows>()
@@ -173,6 +186,55 @@ const sections = computed(() => {
173
186
  }))
174
187
  })
175
188
 
189
+ const totalPages = computed(() =>
190
+ props.pageSize > 0 ? Math.max(1, Math.ceil(sortedRows.value.length / props.pageSize)) : 1,
191
+ )
192
+
193
+ /**
194
+ * La page demandee, ramenee dans les bornes. Un tri ou un filtre qui raccourcit
195
+ * la liste laisserait sinon la table sur une page vide, ce qui se lit comme
196
+ * une recherche sans resultat.
197
+ */
198
+ const currentPage = computed(() => Math.min(Math.max(1, page.value), totalPages.value))
199
+
200
+ const pagedRows = computed(() => {
201
+ if (props.pageSize <= 0) return sortedRows.value
202
+ const start = (currentPage.value - 1) * props.pageSize
203
+ return sortedRows.value.slice(start, start + props.pageSize)
204
+ })
205
+
206
+ /** Le rang de la premiere et de la derniere ligne affichees, pour le compte. */
207
+ const rangeFrom = computed(() =>
208
+ sortedRows.value.length === 0 ? 0 : (currentPage.value - 1) * props.pageSize + 1,
209
+ )
210
+ const rangeTo = computed(() =>
211
+ Math.min(currentPage.value * props.pageSize, sortedRows.value.length),
212
+ )
213
+
214
+ /**
215
+ * Les numeros a poser : les deux voisins de la page courante, les deux bouts,
216
+ * et des ellipses pour le reste. Aligner cinquante numeros ferait une barre
217
+ * plus longue que la table.
218
+ */
219
+ const pageItems = computed<(number | '...')[]>(() => {
220
+ const total = totalPages.value
221
+ const here = currentPage.value
222
+ if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1)
223
+
224
+ const items: (number | '...')[] = [1]
225
+ const from = Math.max(2, here - 1)
226
+ const to = Math.min(total - 1, here + 1)
227
+ if (from > 2) items.push('...')
228
+ for (let i = from; i <= to; i++) items.push(i)
229
+ if (to < total - 1) items.push('...')
230
+ items.push(total)
231
+ return items
232
+ })
233
+
234
+ function goTo(target: number) {
235
+ page.value = Math.min(Math.max(1, target), totalPages.value)
236
+ }
237
+
176
238
  const colspan = computed(
177
239
  () => props.columns.length + (props.selectable ? 1 : 0) + (props.expandable ? 1 : 0),
178
240
  )
@@ -354,9 +416,51 @@ function heatClass(col: RmTableColumn, row: Record<string, any>) {
354
416
  </tbody>
355
417
  </table>
356
418
  </div>
419
+
420
+ <!-- Le compte a gauche, les numeros a droite. Sans le compte, « page 3 »
421
+ ne dit pas combien il reste a lire. -->
422
+ <nav v-if="pageSize > 0 && totalPages > 1" class="rm-table__pager" aria-label="Pagination">
423
+ <span class="rm-table__pager-count">
424
+ {{ rangeFrom }}–{{ rangeTo }} sur {{ sortedRows.length }}
425
+ </span>
426
+
427
+ <div class="rm-table__pager-controls">
428
+ <RmButton
429
+ icon-only
430
+ size="sm"
431
+ variant="secondary"
432
+ prefix-icon="CaretLeft"
433
+ aria-label="Page précédente"
434
+ :disabled="currentPage === 1"
435
+ @click="goTo(currentPage - 1)"
436
+ />
437
+
438
+ <template v-for="(item, i) in pageItems" :key="`${item}-${i}`">
439
+ <span v-if="item === '...'" class="rm-table__pager-gap">…</span>
440
+ <button
441
+ v-else
442
+ type="button"
443
+ class="rm-table__pager-page"
444
+ :class="{ 'is-current': item === currentPage }"
445
+ :aria-current="item === currentPage ? 'page' : undefined"
446
+ @click="goTo(item as number)"
447
+ >{{ item }}</button>
448
+ </template>
449
+
450
+ <RmButton
451
+ icon-only
452
+ size="sm"
453
+ variant="secondary"
454
+ prefix-icon="CaretRight"
455
+ aria-label="Page suivante"
456
+ :disabled="currentPage === totalPages"
457
+ @click="goTo(currentPage + 1)"
458
+ />
459
+ </div>
460
+ </nav>
357
461
  </div>
358
462
  </template>
359
463
 
360
464
  <style scoped>
361
- .rm-table-wrapper{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md,8px);flex:1;overflow:hidden}.rm-table__scroll{overflow:auto}.rm-table{border-collapse:collapse;font-size:var(--font-13,13px);width:100%}.rm-sr-only{height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;clip:rect(0,0,0,0);border:0;white-space:nowrap}.rm-table caption{caption-side:top;color:var(--text-primary);font-size:var(--font-13,13px);font-weight:700;padding:0 0 var(--space-8,8px);text-align:left}.rm-table__th{background:var(--bg-surface-2);border-bottom:1px solid var(--border-default);color:var(--text-secondary);font-size:var(--font-11,11px);font-weight:700;letter-spacing:.08em;padding:12px 14px;text-align:left;text-transform:uppercase;white-space:nowrap}.rm-table--sticky .rm-table__th{border-bottom:none;box-shadow:inset 0 -1px 0 var(--border-default);position:sticky;top:0;z-index:2}.rm-table__th--center{text-align:center}.rm-table__th--num,.rm-table__th--right{text-align:right}.rm-table__th--check,.rm-table__th--expand{padding-right:0;width:36px}.rm-table__sort{align-items:center;background:none;border:none;color:inherit;cursor:pointer;display:inline-flex;font:inherit;gap:var(--space-6,6px);letter-spacing:inherit;padding:0;text-transform:inherit}.rm-table__sort:hover{color:var(--text-primary)}.rm-table__sort:after{border-left:4px solid transparent;border-right:4px solid transparent;border-top:5px solid var(--text-muted);content:"";display:inline-block;height:0;opacity:.5;vertical-align:middle;width:0}.rm-table__th[aria-sort=ascending] .rm-table__sort:after{border-bottom:5px solid var(--color-primary);border-top:none;opacity:1}.rm-table__th[aria-sort=descending] .rm-table__sort:after{border-top:5px solid var(--color-primary);opacity:1}.rm-table a:focus-visible,.rm-table button:focus-visible{border-radius:var(--radius-sm,4px);outline:2px solid var(--color-primary);outline-offset:2px}.rm-table__td{border-bottom:1px solid var(--border-soft);color:var(--text-primary);padding:12px 14px;text-align:left;vertical-align:top}th.rm-table__td{background:transparent;font-weight:700;letter-spacing:0;text-transform:none}.rm-table__td--center{text-align:center}.rm-table__td--right{text-align:right}.rm-table__td--num{font-variant-numeric:tabular-nums;text-align:right}.rm-table__td--check,.rm-table__td--expand{color:var(--text-muted);padding-right:0;width:36px}.rm-table__row--striped>.rm-table__td{background:var(--bg-surface-2)}.rm-table--hoverable .rm-table__row:hover>.rm-table__td,.rm-table__row--selected>.rm-table__td{background:var(--bg-hover)}.rm-table__row--expandable{cursor:pointer}.rm-table__caret{display:inline-flex;transition:transform .16s var(--ease-out,ease)}.rm-table__caret.is-open{color:var(--color-primary);transform:rotate(90deg)}.rm-table__group>th{background:var(--bg-hover);color:var(--color-primary);font-size:var(--font-11,11px);font-weight:700;letter-spacing:.08em;padding:var(--space-8,8px) var(--space-12,12px);text-align:left;text-transform:uppercase}.rm-table__detail>td{background:var(--bg-surface-2);border-bottom:1px solid var(--border-soft);padding:0}.rm-table__detail-body{color:var(--text-secondary);font-size:var(--font-12,12px);line-height:1.6;padding:var(--space-12,12px) var(--space-20,20px)}.rm-table__td--heat-1{background:#fee5e5!important;color:#911616}.rm-table__td--heat-2{background:#ffe7d2!important;color:#8a4309}.rm-table__td--heat-3{background:#fff6cc!important;color:#6b5400}.rm-table__td--heat-4{background:#d8f3dc!important;color:#1b5e25}.rm-table__td--heat-5{background:#b7e4c7!important;color:#0e4b19}:root[data-theme=dark] .rm-table__td--heat-1{background:#3f1414!important;color:#ffb4b4}:root[data-theme=dark] .rm-table__td--heat-2{background:#3d2410!important;color:#ffc58a}:root[data-theme=dark] .rm-table__td--heat-3{background:#3a3210!important;color:#ffe07a}:root[data-theme=dark] .rm-table__td--heat-4{background:#18351f!important;color:#8ae0a4}:root[data-theme=dark] .rm-table__td--heat-5{background:#0f4019!important;color:#b4f0c2}.rm-table--compact .rm-table__td,.rm-table--compact .rm-table__th{font-size:var(--font-12,12px);padding:var(--space-6,6px) var(--space-12,12px)}.rm-table--compact .rm-table__th{font-size:var(--font-10,10px)}.rm-table__bulk{align-items:center;background:var(--bg-hover);border-bottom:1px solid var(--color-violet-pale);display:flex;font-size:var(--font-12,12px);justify-content:space-between;padding:var(--space-8,8px) var(--space-16,16px)}.rm-table__bulk-count b{color:var(--color-primary)}.rm-table__bulk-actions{display:flex;gap:var(--space-4,4px)}.rm-table__empty{color:var(--text-muted);padding:32px 16px;text-align:center}.rm-table__skeleton{height:14px;width:80%}
465
+ .rm-table-wrapper{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md,8px);flex:1;overflow:hidden}.rm-table__scroll{overflow:auto}.rm-table{border-collapse:collapse;font-size:var(--font-13,13px);width:100%}.rm-sr-only{height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;clip:rect(0,0,0,0);border:0;white-space:nowrap}.rm-table caption{caption-side:top;color:var(--text-primary);font-size:var(--font-13,13px);font-weight:700;padding:0 0 var(--space-8,8px);text-align:left}.rm-table__th{background:var(--bg-surface-2);border-bottom:1px solid var(--border-default);color:var(--text-secondary);font-size:var(--font-11,11px);font-weight:700;letter-spacing:.08em;padding:12px 14px;text-align:left;text-transform:uppercase;white-space:nowrap}.rm-table--sticky .rm-table__th{border-bottom:none;box-shadow:inset 0 -1px 0 var(--border-default);position:sticky;top:0;z-index:2}.rm-table__th--center{text-align:center}.rm-table__th--num,.rm-table__th--right{text-align:right}.rm-table__th--check,.rm-table__th--expand{padding-right:0;width:36px}.rm-table__sort{align-items:center;background:none;border:none;color:inherit;cursor:pointer;display:inline-flex;font:inherit;gap:var(--space-6,6px);letter-spacing:inherit;padding:0;text-transform:inherit}.rm-table__sort:hover{color:var(--text-primary)}.rm-table__sort:after{border-left:4px solid transparent;border-right:4px solid transparent;border-top:5px solid var(--text-muted);content:"";display:inline-block;height:0;opacity:.5;vertical-align:middle;width:0}.rm-table__th[aria-sort=ascending] .rm-table__sort:after{border-bottom:5px solid var(--color-primary);border-top:none;opacity:1}.rm-table__th[aria-sort=descending] .rm-table__sort:after{border-top:5px solid var(--color-primary);opacity:1}.rm-table a:focus-visible,.rm-table button:focus-visible{border-radius:var(--radius-sm,4px);outline:2px solid var(--color-primary);outline-offset:2px}.rm-table__td{border-bottom:1px solid var(--border-soft);color:var(--text-primary);padding:12px 14px;text-align:left;vertical-align:top}th.rm-table__td{background:transparent;font-weight:700;letter-spacing:0;text-transform:none}.rm-table__td--center{text-align:center}.rm-table__td--right{text-align:right}.rm-table__td--num{font-variant-numeric:tabular-nums;text-align:right}.rm-table__td--check,.rm-table__td--expand{color:var(--text-muted);padding-right:0;width:36px}.rm-table__row--striped>.rm-table__td{background:var(--bg-surface-2)}.rm-table--hoverable .rm-table__row:hover>.rm-table__td,.rm-table__row--selected>.rm-table__td{background:var(--bg-hover)}.rm-table__row--expandable{cursor:pointer}.rm-table__caret{display:inline-flex;transition:transform .16s var(--ease-out,ease)}.rm-table__caret.is-open{color:var(--color-primary);transform:rotate(90deg)}.rm-table__group>th{background:var(--bg-hover);color:var(--color-primary);font-size:var(--font-11,11px);font-weight:700;letter-spacing:.08em;padding:var(--space-8,8px) var(--space-12,12px);text-align:left;text-transform:uppercase}.rm-table__detail>td{background:var(--bg-surface-2);border-bottom:1px solid var(--border-soft);padding:0}.rm-table__detail-body{color:var(--text-secondary);font-size:var(--font-12,12px);line-height:1.6;padding:var(--space-12,12px) var(--space-20,20px)}.rm-table__td--heat-1{background:#fee5e5!important;color:#911616}.rm-table__td--heat-2{background:#ffe7d2!important;color:#8a4309}.rm-table__td--heat-3{background:#fff6cc!important;color:#6b5400}.rm-table__td--heat-4{background:#d8f3dc!important;color:#1b5e25}.rm-table__td--heat-5{background:#b7e4c7!important;color:#0e4b19}:root[data-theme=dark] .rm-table__td--heat-1{background:#3f1414!important;color:#ffb4b4}:root[data-theme=dark] .rm-table__td--heat-2{background:#3d2410!important;color:#ffc58a}:root[data-theme=dark] .rm-table__td--heat-3{background:#3a3210!important;color:#ffe07a}:root[data-theme=dark] .rm-table__td--heat-4{background:#18351f!important;color:#8ae0a4}:root[data-theme=dark] .rm-table__td--heat-5{background:#0f4019!important;color:#b4f0c2}.rm-table--compact .rm-table__td,.rm-table--compact .rm-table__th{font-size:var(--font-12,12px);padding:var(--space-6,6px) var(--space-12,12px)}.rm-table--compact .rm-table__th{font-size:var(--font-10,10px)}.rm-table__bulk{align-items:center;background:var(--bg-hover);border-bottom:1px solid var(--color-violet-pale);display:flex;font-size:var(--font-12,12px);justify-content:space-between;padding:var(--space-8,8px) var(--space-16,16px)}.rm-table__bulk-count b{color:var(--color-primary)}.rm-table__bulk-actions{display:flex;gap:var(--space-4,4px)}.rm-table__pager{align-items:center;background:var(--bg-surface);border-top:1px solid var(--border-default);display:flex;font-size:var(--font-12,12px);gap:var(--space-12,12px);justify-content:space-between;padding:var(--space-8,8px) var(--space-12,12px)}.rm-table__pager-count{color:var(--text-secondary);font-variant-numeric:tabular-nums}.rm-table__pager-controls{align-items:center;display:flex;gap:var(--space-4,4px)}.rm-table__pager-page{background:none;border:1px solid transparent;border-radius:var(--radius-sm,6px);color:var(--text-secondary);cursor:pointer;font:inherit;font-variant-numeric:tabular-nums;height:28px;min-width:28px;padding:0 6px}.rm-table__pager-page:hover{background:var(--bg-hover);color:var(--color-secondary)}.rm-table__pager-page.is-current{background:var(--color-primary);border-color:var(--color-primary);color:var(--color-text-inverse)}.rm-table__pager-gap{color:var(--text-muted);padding:0 2px}.rm-table__empty{color:var(--text-muted);padding:32px 16px;text-align:center}.rm-table__skeleton{height:14px;width:80%}
362
466
  </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekkare/romulus",
3
- "version": "0.1.62",
3
+ "version": "0.1.64",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",