@tekkare/romulus 0.1.86 → 0.1.88

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.85",
7
+ "version": "0.1.87",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
@@ -12,6 +12,12 @@
12
12
  et `ph-file-csv` ne se confondent pas en noir et blanc. Elle vient des tokens
13
13
  du theme et non d'un hexadecimal en dur, pour suivre le mode nuit.
14
14
 
15
+ Elle passe par la prop `color` de RmIcon et non par une classe : le style
16
+ scope de ce fichier n'atteint pas le `<svg>` que RmIcon monte derriere son
17
+ `<ClientOnly>`, et l'icone retombait sur la couleur du texte. Les classes
18
+ posees sur RmIcon ne servent donc qu'a la mise en place, jamais a la
19
+ couleur.
20
+
15
21
  Le composant ne navigue pas et n'ouvre rien lui-meme : il emet `select`, la
16
22
  page decide (un apercu dans un tiroir, un onglet, un telechargement).
17
23
  -->
@@ -46,18 +52,18 @@ const emit = defineEmits<{ select: [] }>()
46
52
  * neutre dit « document » sans se tromper, ce qu'une icone approchante ne
47
53
  * ferait pas.
48
54
  */
49
- const FORMATS: { match: string[]; icon: string; tone: string }[] = [
50
- { match: ['pdf'], icon: 'FilePdf', tone: 'pdf' },
51
- { match: ['csv'], icon: 'FileCsv', tone: 'csv' },
52
- { match: ['sheet', 'excel', 'xls'], icon: 'FileXls', tone: 'sheet' },
53
- { match: ['word', 'doc'], icon: 'FileDoc', tone: 'doc' },
55
+ const FORMATS: { match: string[]; icon: string; color: string }[] = [
56
+ { match: ['pdf'], icon: 'FilePdf', color: 'var(--color-danger)' },
57
+ { match: ['csv'], icon: 'FileCsv', color: 'var(--color-info)' },
58
+ { match: ['sheet', 'excel', 'xls'], icon: 'FileXls', color: 'var(--color-success)' },
59
+ { match: ['word', 'doc'], icon: 'FileDoc', color: 'var(--color-primary)' },
54
60
  ]
55
61
 
56
62
  const format = computed(() => {
57
63
  const value = props.format.toLowerCase()
58
64
  return (
59
65
  FORMATS.find(entry => entry.match.some(word => value.includes(word)))
60
- ?? { icon: 'FileText', tone: 'plain' }
66
+ ?? { icon: 'FileText', color: 'var(--text-muted)' }
61
67
  )
62
68
  })
63
69
 
@@ -70,23 +76,17 @@ const ACTION_ICONS: Record<string, string> = {
70
76
 
71
77
  <template>
72
78
  <button type="button" class="rm-file-row" @click="emit('select')">
73
- <RmIcon
74
- class="rm-file-row__icon"
75
- :class="`rm-file-row__icon--${format.tone}`"
76
- :name="format.icon"
77
- :size="16"
78
- />
79
+ <span class="rm-file-row__icon">
80
+ <RmIcon :name="format.icon" :size="16" :color="format.color" />
81
+ </span>
79
82
  <span class="rm-file-row__name">{{ name }}</span>
80
83
  <span v-if="size" class="rm-file-row__size">{{ size }}</span>
81
- <RmIcon
82
- v-if="action !== 'none'"
83
- class="rm-file-row__action"
84
- :name="ACTION_ICONS[action]"
85
- :size="14"
86
- />
84
+ <span v-if="action !== 'none'" class="rm-file-row__action">
85
+ <RmIcon :name="ACTION_ICONS[action]" :size="14" color="var(--text-muted)" />
86
+ </span>
87
87
  </button>
88
88
  </template>
89
89
 
90
90
  <style scoped>
91
- .rm-file-row{align-items:center;background:transparent;border:none;border-radius:var(--radius-md);color:var(--text-primary);cursor:pointer;display:flex;font-family:var(--font-sans);font-size:var(--font-13);gap:8px;line-height:1.4;padding:8px;text-align:left;transition:background-color var(--duration-fast) linear;width:100%}.rm-file-row:hover{background:var(--bg-surface-2)}.rm-file-row__icon{color:var(--text-muted);flex:none}.rm-file-row__icon--pdf{color:var(--color-danger)}.rm-file-row__icon--csv{color:var(--color-info)}.rm-file-row__icon--sheet{color:var(--color-success)}.rm-file-row__icon--doc{color:var(--color-primary)}.rm-file-row__name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rm-file-row__size{font-size:var(--font-12)}.rm-file-row__action,.rm-file-row__size{color:var(--text-muted);flex:none}
91
+ .rm-file-row{align-items:center;background:transparent;border:none;border-radius:var(--radius-md);color:var(--text-primary);cursor:pointer;display:flex;font-family:var(--font-sans);font-size:var(--font-13);gap:8px;line-height:1.4;padding:8px;text-align:left;transition:background-color var(--duration-fast) linear;width:100%}.rm-file-row:hover{background:var(--bg-surface-2)}.rm-file-row__action,.rm-file-row__icon{display:inline-flex;flex:none}.rm-file-row__name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rm-file-row__size{color:var(--text-muted);flex:none;font-size:var(--font-12)}
92
92
  </style>
@@ -0,0 +1,380 @@
1
+ <!--
2
+ RmNotes - le panneau « Notes », la documentation d'un ecran en un seul endroit.
3
+
4
+ Une app metier accumulait un onglet par genre d'explication : Infos,
5
+ Metrics, Tips, Methodologie. Quatre tuiles au rail pour quatre textes courts,
6
+ et le lecteur devait deviner sous laquelle sa reponse se trouvait — la
7
+ definition d'un indicateur pouvait aussi bien tenir dans « Metrics » que dans
8
+ « Infos ». Le panneau les rassemble : une seule tuile, une liste de notes,
9
+ et le genre devient une categorie qu'on filtre au lieu d'un onglet qu'on
10
+ cherche.
11
+
12
+ Le composant ne connait pas l'ecran : l'appelant lui donne les notes deja
13
+ triees pour la page ouverte, deja traduites. La recherche et les pastilles
14
+ travaillent sur cette liste en memoire, sans aller-retour.
15
+
16
+ Le temps de lecture est calcule a partir du texte des notes, jamais pose a la
17
+ main : une note qu'on rallonge annoncerait sinon une duree fausse. La date,
18
+ elle, vient de l'appelant — c'est une date de revision editoriale, que rien
19
+ dans le contenu ne permet de deviner.
20
+ -->
21
+ <script setup lang="ts">
22
+ import { computed, ref } from 'vue'
23
+
24
+ export type RmNoteTone = 'primary' | 'info' | 'warning' | 'success' | 'neutral'
25
+
26
+ export type RmNoteBlock =
27
+ /** Paragraphe. `**gras**` est reconnu, rien d'autre. */
28
+ | { type: 'text', text: string }
29
+ /** Liste a puces. Chaque entree accepte le meme gras. */
30
+ | { type: 'list', items: string[] }
31
+ /** Liste de definitions : le terme en gras, sa definition a la suite. */
32
+ | { type: 'terms', items: { term: string, def?: string }[] }
33
+ /** Encadre : un avertissement, une limite, une precaution de lecture. */
34
+ | { type: 'callout', text: string, tone?: 'info' | 'warning', title?: string }
35
+ /** Exemple, rendu en fonte a chasse fixe sur fond teinte. */
36
+ | { type: 'example', text: string }
37
+ /** Renvoi vers un ecran de l'app. Le clic remonte en `navigate`. */
38
+ | { type: 'link', to: string, label: string }
39
+
40
+ export interface RmNoteSection {
41
+ /** Intitule de section, en capitales dans la note depliee. */
42
+ title?: string
43
+ blocks: RmNoteBlock[]
44
+ }
45
+
46
+ export interface RmNote {
47
+ id: string
48
+ title: string
49
+ /** Accroche lue liste repliee : ce que la note repond, en une phrase. */
50
+ summary: string
51
+ /** Cle de categorie, a rapprocher de `categories`. */
52
+ category: string
53
+ /** Date ISO de derniere revision editoriale. */
54
+ updated?: string
55
+ /** Icone Phosphor. Par defaut celle de la categorie. */
56
+ icon?: string
57
+ /** Teinte de la pastille. Par defaut celle de la categorie. */
58
+ tone?: RmNoteTone
59
+ sections: RmNoteSection[]
60
+ }
61
+
62
+ export interface RmNoteCategory {
63
+ key: string
64
+ label: string
65
+ icon?: string
66
+ tone?: RmNoteTone
67
+ }
68
+
69
+ const props = withDefaults(defineProps<{
70
+ /** Notes de l'ecran ouvert, deja traduites et ordonnees. */
71
+ notes: RmNote[]
72
+ /** Categories connues, dans l'ordre des pastilles. Celles sans note sont masquees. */
73
+ categories?: RmNoteCategory[]
74
+ title?: string
75
+ /** Phrase d'introduction : qui ecrit ces notes, et ce qu'elles couvrent. */
76
+ description?: string
77
+ /** Pastille d'auteur en haut a droite (« TEKKARE »). Vide : pas de pastille. */
78
+ badge?: string
79
+ lang?: 'fr' | 'en'
80
+ }>(), {
81
+ categories: () => [],
82
+ title: 'Notes',
83
+ description: undefined,
84
+ badge: undefined,
85
+ lang: 'fr',
86
+ })
87
+
88
+ const emit = defineEmits<{ navigate: [to: string] }>()
89
+
90
+ const WORDS = {
91
+ fr: {
92
+ all: 'Toutes',
93
+ search: 'Rechercher dans les notes...',
94
+ updated: 'MAJ',
95
+ reading: 'min de lecture',
96
+ expand: 'Déplier la note',
97
+ collapse: 'Replier la note',
98
+ emptyTitle: 'Aucune note pour cet écran',
99
+ emptyText: "Les notes sont rattachées à la page affichée : changez d'écran pour en voir d'autres.",
100
+ noMatch: 'Aucune note ne correspond',
101
+ noMatchText: 'Essayez un autre terme, ou revenez à « Toutes ».',
102
+ },
103
+ en: {
104
+ all: 'All',
105
+ search: 'Search the notes...',
106
+ updated: 'Updated',
107
+ reading: 'min read',
108
+ expand: 'Expand note',
109
+ collapse: 'Collapse note',
110
+ emptyTitle: 'No note for this screen',
111
+ emptyText: 'Notes belong to the page on display: switch screens to see others.',
112
+ noMatch: 'No note matches',
113
+ noMatchText: 'Try another term, or go back to "All".',
114
+ },
115
+ } as const
116
+
117
+ const T = computed(() => WORDS[props.lang] ?? WORDS.fr)
118
+
119
+ const search = ref('')
120
+ const category = ref('all')
121
+ const open = ref<string[]>([])
122
+
123
+ function toggle(id: string) {
124
+ open.value = open.value.includes(id)
125
+ ? open.value.filter(item => item !== id)
126
+ : [...open.value, id]
127
+ }
128
+
129
+ const descriptor = computed(() => {
130
+ const map = new Map<string, RmNoteCategory>()
131
+ for (const item of props.categories) map.set(item.key, item)
132
+ return map
133
+ })
134
+
135
+ /** Tout le texte d'une note, pour la recherche et le temps de lecture. */
136
+ function plainText(note: RmNote) {
137
+ const parts = [note.title, note.summary]
138
+ for (const section of note.sections) {
139
+ if (section.title) parts.push(section.title)
140
+ for (const block of section.blocks) {
141
+ if (block.type === 'text' || block.type === 'callout' || block.type === 'example') parts.push(block.text)
142
+ else if (block.type === 'list') parts.push(block.items.join(' '))
143
+ else if (block.type === 'terms') parts.push(block.items.map(item => `${item.term} ${item.def ?? ''}`).join(' '))
144
+ else if (block.type === 'link') parts.push(block.label)
145
+ }
146
+ }
147
+ return parts.join(' ')
148
+ }
149
+
150
+ /** 200 mots par minute, une minute au minimum : « 0 min » ne veut rien dire. */
151
+ function readingTime(note: RmNote) {
152
+ const words = plainText(note).trim().split(/\s+/).filter(Boolean).length
153
+ return Math.max(1, Math.round(words / 200))
154
+ }
155
+
156
+ const shown = computed(() => {
157
+ const needle = search.value.trim().toLowerCase()
158
+ return props.notes.filter((note) => {
159
+ if (category.value !== 'all' && note.category !== category.value) return false
160
+ if (!needle) return true
161
+ return plainText(note).toLowerCase().includes(needle)
162
+ })
163
+ })
164
+
165
+ /** Pastilles : « Toutes » puis les seules categories qui ont une note ici. */
166
+ const chips = computed(() => {
167
+ const counts = new Map<string, number>()
168
+ for (const note of props.notes) counts.set(note.category, (counts.get(note.category) ?? 0) + 1)
169
+
170
+ const ordered = props.categories.length
171
+ ? props.categories.filter(item => counts.has(item.key))
172
+ : [...counts.keys()].map(key => ({ key, label: key } as RmNoteCategory))
173
+
174
+ return [
175
+ { key: 'all', label: T.value.all, icon: undefined, count: props.notes.length },
176
+ ...ordered.map(item => ({ key: item.key, label: item.label, icon: item.icon, count: counts.get(item.key) ?? 0 })),
177
+ ]
178
+ })
179
+
180
+ function noteIcon(note: RmNote) {
181
+ return note.icon ?? descriptor.value.get(note.category)?.icon ?? 'Note'
182
+ }
183
+
184
+ function noteTone(note: RmNote): RmNoteTone {
185
+ return note.tone ?? descriptor.value.get(note.category)?.tone ?? 'primary'
186
+ }
187
+
188
+ function categoryLabel(note: RmNote) {
189
+ return descriptor.value.get(note.category)?.label ?? note.category
190
+ }
191
+
192
+ function updatedLabel(note: RmNote) {
193
+ const time = note.updated ? new Date(note.updated).getTime() : Number.NaN
194
+ if (!Number.isFinite(time) || time <= 0) return ''
195
+ return new Date(time).toLocaleDateString(props.lang === 'fr' ? 'fr-FR' : 'en-GB', {
196
+ day: 'numeric',
197
+ month: 'long',
198
+ year: 'numeric',
199
+ })
200
+ }
201
+
202
+ /**
203
+ * Le seul balisage reconnu dans une note est `**gras**`. Le texte est decoupe
204
+ * en morceaux rendus par `v-for` plutot que par `v-html` : les notes viennent
205
+ * du code de l'app aujourd'hui, d'un CMS demain, et un panneau qui interprete
206
+ * du HTML est un panneau qui l'executera le jour ou la source change.
207
+ */
208
+ function inline(text: string) {
209
+ return text.split(/(\*\*[^*]+\*\*)/g)
210
+ .filter(Boolean)
211
+ .map((chunk, index) => ({
212
+ key: index,
213
+ bold: chunk.startsWith('**') && chunk.endsWith('**'),
214
+ text: chunk.startsWith('**') && chunk.endsWith('**') ? chunk.slice(2, -2) : chunk,
215
+ }))
216
+ }
217
+
218
+ function onLink(to: string) {
219
+ emit('navigate', to)
220
+ }
221
+ </script>
222
+
223
+ <template>
224
+ <div class="rm-notes">
225
+ <div class="rm-notes__head">
226
+ <h2 v-if="title" class="rm-notes__title">{{ title }}</h2>
227
+ <span v-if="badge" class="rm-notes__badge">
228
+ <RmIcon name="ShieldCheck" :size="14" weight="fill" />
229
+ {{ badge }}
230
+ </span>
231
+ </div>
232
+
233
+ <p v-if="description" class="rm-notes__desc">{{ description }}</p>
234
+
235
+ <label class="rm-notes__search">
236
+ <RmIcon name="MagnifyingGlass" :size="14" />
237
+ <input v-model="search" type="text" :placeholder="T.search">
238
+ </label>
239
+
240
+ <div class="rm-notes__chips" role="group">
241
+ <button
242
+ v-for="chip in chips"
243
+ :key="chip.key"
244
+ type="button"
245
+ class="rm-notes__chip"
246
+ :class="{ 'rm-notes__chip--on': category === chip.key }"
247
+ :aria-pressed="category === chip.key"
248
+ @click="category = chip.key"
249
+ >
250
+ <RmIcon v-if="chip.icon" :name="chip.icon" :size="13" />
251
+ {{ chip.label }}
252
+ <span v-if="chip.key === 'all'" class="rm-notes__chip-count">{{ chip.count }}</span>
253
+ </button>
254
+ </div>
255
+
256
+ <RmEmptyState
257
+ v-if="!notes.length"
258
+ framed
259
+ tone="primary"
260
+ icon="Notebook"
261
+ :title="T.emptyTitle"
262
+ :text="T.emptyText"
263
+ />
264
+
265
+ <RmEmptyState
266
+ v-else-if="!shown.length"
267
+ framed
268
+ icon="MagnifyingGlass"
269
+ :title="T.noMatch"
270
+ :text="T.noMatchText"
271
+ />
272
+
273
+ <div v-else class="rm-notes__list">
274
+ <article
275
+ v-for="note in shown"
276
+ :key="note.id"
277
+ class="rm-notes__note"
278
+ :class="{ 'rm-notes__note--open': open.includes(note.id) }"
279
+ >
280
+ <header class="rm-notes__note-head">
281
+ <button
282
+ type="button"
283
+ class="rm-notes__note-id"
284
+ :aria-expanded="open.includes(note.id)"
285
+ :aria-controls="`rm-note-${note.id}`"
286
+ @click="toggle(note.id)"
287
+ >
288
+ <span class="rm-notes__note-icon" :class="`rm-notes__note-icon--${noteTone(note)}`">
289
+ <RmIcon :name="noteIcon(note)" :size="18" weight="fill" />
290
+ </span>
291
+ <span class="rm-notes__note-text">
292
+ <span class="rm-notes__note-title">{{ note.title }}</span>
293
+ <span class="rm-notes__note-meta">
294
+ <span class="rm-notes__tag" :class="`rm-notes__tag--${noteTone(note)}`">
295
+ {{ categoryLabel(note) }}
296
+ </span>
297
+ <span v-if="updatedLabel(note)" class="rm-notes__cell">
298
+ <RmIcon name="Clock" :size="13" />
299
+ {{ T.updated }} {{ updatedLabel(note) }}
300
+ </span>
301
+ <span class="rm-notes__cell">
302
+ <RmIcon name="BookOpenText" :size="13" />
303
+ {{ readingTime(note) }} {{ T.reading }}
304
+ </span>
305
+ </span>
306
+ </span>
307
+ </button>
308
+
309
+ <button
310
+ type="button"
311
+ class="rm-notes__caret"
312
+ :aria-expanded="open.includes(note.id)"
313
+ :aria-controls="`rm-note-${note.id}`"
314
+ :aria-label="open.includes(note.id) ? T.collapse : T.expand"
315
+ :title="open.includes(note.id) ? T.collapse : T.expand"
316
+ @click="toggle(note.id)"
317
+ >
318
+ <RmIcon :name="open.includes(note.id) ? 'CaretUp' : 'CaretDown'" :size="14" />
319
+ </button>
320
+ </header>
321
+
322
+ <p class="rm-notes__summary">{{ note.summary }}</p>
323
+
324
+ <div v-if="open.includes(note.id)" :id="`rm-note-${note.id}`" class="rm-notes__body">
325
+ <section v-for="(section, index) in note.sections" :key="index" class="rm-notes__section">
326
+ <p v-if="section.title" class="rm-notes__eyebrow">{{ section.title }}</p>
327
+
328
+ <template v-for="(block, blockIndex) in section.blocks" :key="blockIndex">
329
+ <p v-if="block.type === 'text'" class="rm-notes__p">
330
+ <span v-for="part in inline(block.text)" :key="part.key">
331
+ <b v-if="part.bold">{{ part.text }}</b>
332
+ <template v-else>{{ part.text }}</template>
333
+ </span>
334
+ </p>
335
+
336
+ <ul v-else-if="block.type === 'list'" class="rm-notes__ul">
337
+ <li v-for="(item, itemIndex) in block.items" :key="itemIndex">
338
+ <span v-for="part in inline(item)" :key="part.key">
339
+ <b v-if="part.bold">{{ part.text }}</b>
340
+ <template v-else>{{ part.text }}</template>
341
+ </span>
342
+ </li>
343
+ </ul>
344
+
345
+ <ul v-else-if="block.type === 'terms'" class="rm-notes__ul rm-notes__ul--terms">
346
+ <li v-for="(item, itemIndex) in block.items" :key="itemIndex">
347
+ <b>{{ item.term }}</b><template v-if="item.def"> : {{ item.def }}</template>
348
+ </li>
349
+ </ul>
350
+
351
+ <RmCallout v-else-if="block.type === 'callout'" :tone="block.tone ?? 'info'">
352
+ <b v-if="block.title">{{ block.title }} </b>
353
+ <span v-for="part in inline(block.text)" :key="part.key">
354
+ <b v-if="part.bold">{{ part.text }}</b>
355
+ <template v-else>{{ part.text }}</template>
356
+ </span>
357
+ </RmCallout>
358
+
359
+ <p v-else-if="block.type === 'example'" class="rm-notes__example">{{ block.text }}</p>
360
+
361
+ <a
362
+ v-else-if="block.type === 'link'"
363
+ class="rm-notes__link"
364
+ :href="block.to"
365
+ @click.prevent="onLink(block.to)"
366
+ >
367
+ <RmIcon name="ArrowUpRight" :size="14" />
368
+ <span>{{ block.label }}</span>
369
+ </a>
370
+ </template>
371
+ </section>
372
+ </div>
373
+ </article>
374
+ </div>
375
+ </div>
376
+ </template>
377
+
378
+ <style scoped>
379
+ .rm-notes{align-self:stretch;flex-direction:column;font-family:var(--font-sans)}.rm-notes,.rm-notes__head{display:flex;gap:var(--space-12)}.rm-notes__head{align-items:center;justify-content:space-between}.rm-notes__title{color:var(--text-primary);font-family:var(--font-serif);font-size:var(--font-22);font-weight:var(--weight-bold);margin:0}.rm-notes__badge{align-items:center;background:var(--bg-hover);border-radius:var(--radius-pill);color:var(--color-secondary);display:inline-flex;font-size:var(--font-10);font-weight:var(--weight-bold);gap:var(--space-4);letter-spacing:.06em;padding:4px 10px;text-transform:uppercase}.rm-notes__desc{color:var(--text-secondary);font-size:var(--font-13);line-height:1.5;margin:0}.rm-notes__search{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md);color:var(--text-muted);display:flex;gap:var(--space-8);padding:var(--space-8) var(--space-12)}.rm-notes__search:focus-within{border-color:var(--color-secondary);box-shadow:0 0 0 3px rgba(var(--brand-rgb),.1)}.rm-notes__search input{background:none;border:none;color:var(--text-primary);flex:1;font-family:inherit;font-size:var(--font-13);min-width:0;outline:none}.rm-notes__search input::-moz-placeholder{color:var(--text-muted)}.rm-notes__search input::placeholder{color:var(--text-muted)}.rm-notes__chips{display:flex;flex-wrap:wrap;gap:var(--space-6)}.rm-notes__chip{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-pill);color:var(--text-secondary);cursor:pointer;display:inline-flex;font-family:inherit;font-size:var(--font-12);gap:var(--space-4);padding:5px 10px}.rm-notes__chip:hover{background:var(--bg-surface-3)}.rm-notes__chip--on{background:var(--bg-hover);border-color:var(--color-violet-pale);color:var(--color-primary);font-weight:var(--weight-semibold)}.rm-notes__chip-count{font-variant-numeric:tabular-nums;opacity:.8}.rm-notes__list,.rm-notes__note{display:flex;flex-direction:column;gap:var(--space-8)}.rm-notes__note{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-xl);padding:var(--space-16);transition:border-color var(--duration-fast) var(--ease-out)}.rm-notes__note:hover{border-color:var(--color-violet-pale)}.rm-notes__note--open{border-color:var(--color-secondary)}.rm-notes__note-head,.rm-notes__note-id{align-items:flex-start;display:flex;gap:var(--space-12)}.rm-notes__note-id{background:none;border:none;cursor:pointer;flex:1;font-family:inherit;min-width:0;padding:0;text-align:left}.rm-notes__note-icon{align-items:center;border-radius:var(--radius-lg);display:flex;flex-shrink:0;height:36px;justify-content:center;width:36px}.rm-notes__note-icon--primary{background:var(--bg-hover);color:var(--color-secondary)}.rm-notes__note-icon--info{background:var(--color-info-bg);color:var(--color-info)}.rm-notes__note-icon--warning{background:var(--color-warning-bg);color:var(--color-warning)}.rm-notes__note-icon--success{background:var(--color-success-bg);color:var(--color-success)}.rm-notes__note-icon--neutral{background:var(--bg-surface-2);color:var(--text-secondary)}.rm-notes__note-text{display:flex;flex-direction:column;gap:var(--space-6);min-width:0}.rm-notes__note-title{color:var(--text-primary);font-family:var(--font-serif);font-size:var(--font-16);font-weight:var(--weight-bold)}.rm-notes__note-meta{align-items:center;color:var(--text-muted);display:flex;flex-wrap:wrap;font-size:var(--font-11);gap:var(--space-8)}.rm-notes__cell{align-items:center;display:inline-flex;gap:var(--space-4)}.rm-notes__tag{border-radius:var(--radius-pill);font-size:var(--font-10);font-weight:var(--weight-bold);letter-spacing:.04em;padding:2px var(--space-6);text-transform:uppercase}.rm-notes__tag--primary{background:var(--bg-hover);color:var(--color-secondary)}.rm-notes__tag--info{background:var(--color-info-bg);color:var(--color-info)}.rm-notes__tag--warning{background:var(--color-warning-bg);color:var(--color-warning)}.rm-notes__tag--success{background:var(--color-success-bg);color:var(--color-success)}.rm-notes__tag--neutral{background:var(--bg-surface-2);color:var(--text-secondary)}.rm-notes__caret{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md);color:var(--text-secondary);cursor:pointer;display:flex;flex-shrink:0;height:28px;justify-content:center;width:28px}.rm-notes__caret:hover{background:var(--bg-surface-3)}.rm-notes__summary{color:var(--text-secondary);font-size:var(--font-13);line-height:1.6;margin:0}.rm-notes__body{border-top:1px solid var(--border-soft);display:flex;flex-direction:column;gap:var(--space-16);padding-top:var(--space-12)}.rm-notes__section{display:flex;flex-direction:column;gap:var(--space-8)}.rm-notes__eyebrow{color:var(--text-muted);font-size:var(--font-10);font-weight:var(--weight-bold);letter-spacing:.08em;margin:0;text-transform:uppercase}.rm-notes__p,.rm-notes__ul{color:var(--text-secondary);font-size:var(--font-13);line-height:1.65;margin:0}.rm-notes__ul{padding-left:var(--space-20)}.rm-notes__ul--terms{list-style:none;padding-left:0}.rm-notes__p b,.rm-notes__ul b{color:var(--text-primary)}.rm-notes__example{background:var(--bg-surface-3);border-left:3px solid var(--color-primary);border-radius:var(--radius-sm);color:var(--text-secondary);font-family:var(--font-mono);font-size:var(--font-12);margin:0;padding:var(--space-8) var(--space-12)}.rm-notes__link{align-items:center;color:var(--color-primary);cursor:pointer;display:inline-flex;font-size:var(--font-13);font-weight:var(--weight-semibold);gap:var(--space-4);text-decoration:underline;text-underline-offset:3px}
380
+ </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekkare/romulus",
3
- "version": "0.1.86",
3
+ "version": "0.1.88",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",