intelligent-system-design-language 0.3.21 → 0.3.23

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.
Files changed (116) hide show
  1. package/.claude/agents/langium-language-designer.md +38 -38
  2. package/.claude/agents/typescript-vscode-expert.md +29 -29
  3. package/.claude/agents/ui-ux-designer.md +36 -36
  4. package/.claude/settings.local.json +33 -33
  5. package/.idea/inspectionProfiles/Project_Default.xml +6 -6
  6. package/.idea/isdl.iml +13 -13
  7. package/.idea/modules.xml +8 -8
  8. package/.idea/vcs.xml +6 -6
  9. package/.idea/watcherTasks.xml +3 -3
  10. package/.vscodeignore +18 -18
  11. package/LICENSE +673 -673
  12. package/README.md +86 -86
  13. package/bin/cli.js +4 -4
  14. package/bin/lsp.js +8 -8
  15. package/out/_backgrounds.scss +91 -91
  16. package/out/_handlebars.scss +497 -497
  17. package/out/_isdlStyles.scss +1444 -1381
  18. package/out/_vuetifyOverrides.scss +425 -425
  19. package/out/_vuetifyStyles.scss +31957 -31957
  20. package/out/cli/components/_backgrounds.scss +91 -91
  21. package/out/cli/components/_handlebars.scss +497 -497
  22. package/out/cli/components/_isdlStyles.scss +1444 -1381
  23. package/out/cli/components/_vuetifyOverrides.scss +425 -425
  24. package/out/cli/components/_vuetifyStyles.scss +31957 -31957
  25. package/out/cli/components/active-effect-sheet-generator.js +453 -453
  26. package/out/cli/components/chat-card-generator.js +654 -651
  27. package/out/cli/components/chat-card-generator.js.map +1 -1
  28. package/out/cli/components/css-generator.js +4 -4
  29. package/out/cli/components/damage-roll-generator.js +160 -160
  30. package/out/cli/components/datamodel-generator.js +264 -257
  31. package/out/cli/components/datamodel-generator.js.map +1 -1
  32. package/out/cli/components/derived-data-generator.js +923 -923
  33. package/out/cli/components/hotbar-drop-hook-generator.js +82 -82
  34. package/out/cli/components/init-hook-generator.js +495 -495
  35. package/out/cli/components/language-generator.js +1 -1
  36. package/out/cli/components/language-generator.js.map +1 -1
  37. package/out/cli/components/measured-template-preview.js +221 -221
  38. package/out/cli/components/method-generator.js +979 -887
  39. package/out/cli/components/method-generator.js.map +1 -1
  40. package/out/cli/components/ready-hook-generator.js +404 -404
  41. package/out/cli/components/token-generator.js +116 -116
  42. package/out/cli/components/vue/base-components/vue-attribute.js +138 -138
  43. package/out/cli/components/vue/base-components/vue-boolean.js +64 -64
  44. package/out/cli/components/vue/base-components/vue-calculator.js +93 -93
  45. package/out/cli/components/vue/base-components/vue-damage-application.js +356 -356
  46. package/out/cli/components/vue/base-components/vue-damage-bonuses.js +165 -165
  47. package/out/cli/components/vue/base-components/vue-damage-resistances.js +196 -196
  48. package/out/cli/components/vue/base-components/vue-damage-track.js +121 -121
  49. package/out/cli/components/vue/base-components/vue-date-time.js +42 -42
  50. package/out/cli/components/vue/base-components/vue-dice.js +98 -98
  51. package/out/cli/components/vue/base-components/vue-die.js +73 -73
  52. package/out/cli/components/vue/base-components/vue-document-choice.js +149 -149
  53. package/out/cli/components/vue/base-components/vue-document-choices.js +179 -179
  54. package/out/cli/components/vue/base-components/vue-document-link.js +60 -60
  55. package/out/cli/components/vue/base-components/vue-extended-choice.js +88 -88
  56. package/out/cli/components/vue/base-components/vue-inventory.js +519 -519
  57. package/out/cli/components/vue/base-components/vue-macro-choice.js +138 -138
  58. package/out/cli/components/vue/base-components/vue-measured-template.js +530 -530
  59. package/out/cli/components/vue/base-components/vue-money.js +483 -483
  60. package/out/cli/components/vue/base-components/vue-number.js +174 -174
  61. package/out/cli/components/vue/base-components/vue-paperdoll.js +43 -43
  62. package/out/cli/components/vue/base-components/vue-parent-property-reference.js +76 -76
  63. package/out/cli/components/vue/base-components/vue-prosemirror.js +18 -18
  64. package/out/cli/components/vue/base-components/vue-resource.js +136 -136
  65. package/out/cli/components/vue/base-components/vue-roll-visualizer.js +286 -109
  66. package/out/cli/components/vue/base-components/vue-roll-visualizer.js.map +1 -1
  67. package/out/cli/components/vue/base-components/vue-self-property-reference.js +62 -62
  68. package/out/cli/components/vue/base-components/vue-string-choice.js +98 -98
  69. package/out/cli/components/vue/base-components/vue-string-choices.js +203 -203
  70. package/out/cli/components/vue/base-components/vue-string.js +60 -60
  71. package/out/cli/components/vue/base-components/vue-text-field.js +53 -53
  72. package/out/cli/components/vue/base-components/vue-tracker.js +431 -431
  73. package/out/cli/components/vue/vue-action-component-generator.js +64 -64
  74. package/out/cli/components/vue/vue-active-effect-sheet-generator.js +856 -856
  75. package/out/cli/components/vue/vue-datatable-sheet-class-generator.js +292 -292
  76. package/out/cli/components/vue/vue-datatable2-component-generator.js +824 -824
  77. package/out/cli/components/vue/vue-document-creation-app.js +121 -121
  78. package/out/cli/components/vue/vue-document-creation-sheet.js +94 -94
  79. package/out/cli/components/vue/vue-generator.js +40 -40
  80. package/out/cli/components/vue/vue-mixin.js +296 -296
  81. package/out/cli/components/vue/vue-pinned-datatable-component-generator.js +260 -260
  82. package/out/cli/components/vue/vue-prompt-generator.js +91 -76
  83. package/out/cli/components/vue/vue-prompt-generator.js.map +1 -1
  84. package/out/cli/components/vue/vue-prompt-sheet-class-generator.js +317 -317
  85. package/out/cli/components/vue/vue-sheet-application-generator.js +1177 -1167
  86. package/out/cli/components/vue/vue-sheet-application-generator.js.map +1 -1
  87. package/out/cli/components/vue/vue-sheet-class-generator.js +510 -510
  88. package/out/cli/generator.js +438 -433
  89. package/out/cli/generator.js.map +1 -1
  90. package/out/extension/github/githubAuthProvider.js +71 -29
  91. package/out/extension/github/githubAuthProvider.js.map +1 -1
  92. package/out/extension/github/githubGistManager.js +4 -3
  93. package/out/extension/github/githubGistManager.js.map +1 -1
  94. package/out/extension/github/githubManager.js +40 -38
  95. package/out/extension/github/githubManager.js.map +1 -1
  96. package/out/extension/github/githubQuickActions.js +120 -120
  97. package/out/extension/github/system-workflow.yml +47 -47
  98. package/out/extension/main.cjs +909 -532
  99. package/out/extension/main.cjs.map +3 -3
  100. package/out/extension/package.json +419 -419
  101. package/out/language/generated/ast.js +51 -2
  102. package/out/language/generated/ast.js.map +1 -1
  103. package/out/language/generated/grammar.js +14240 -13991
  104. package/out/language/generated/grammar.js.map +1 -1
  105. package/out/language/intelligent-system-design-language-validator.js +32 -2
  106. package/out/language/intelligent-system-design-language-validator.js.map +1 -1
  107. package/out/language/isdl-scope-provider.js +14 -1
  108. package/out/language/isdl-scope-provider.js.map +1 -1
  109. package/out/language/main.cjs +913 -569
  110. package/out/language/main.cjs.map +3 -3
  111. package/out/package.json +419 -419
  112. package/out/progressbar.min.js +6 -6
  113. package/out/styles.scss +762 -747
  114. package/out/test/validating/diagnostics.test.js +40 -0
  115. package/out/test/validating/diagnostics.test.js.map +1 -1
  116. package/package.json +419 -419
@@ -7,525 +7,525 @@ export default function generateInventoryComponent(destination, entry) {
7
7
  if (!fs.existsSync(generatedFileDir)) {
8
8
  fs.mkdirSync(generatedFileDir, { recursive: true });
9
9
  }
10
- const fileNode = expandToNode `
11
- <script setup>
12
- import { ref, computed, inject, onMounted, onUnmounted } from "vue";
13
-
14
- const props = defineProps({
15
- label: String,
16
- systemPath: String,
17
- context: Object,
18
- editMode: Boolean,
19
- icon: String,
20
- color: String,
21
- disabled: Boolean,
22
- maxSlots: {
23
- type: Number,
24
- default: 20
25
- },
26
- columns: {
27
- type: Number,
28
- default: 5
29
- },
30
- rows: {
31
- type: Number,
32
- default: 3
33
- },
34
- slotSize: {
35
- type: Number,
36
- default: 60
37
- },
38
- documentType: String,
39
- whereExpression: String,
40
- globalAllowed: Boolean,
41
- quantityField: String,
42
- moneyField: String,
43
- moneyFieldLabel: String,
44
- moneyFieldIcon: String,
45
- sumProperties: Array,
46
- sumMax: Array,
47
- sortProperty: String,
48
- sortOrder: {
49
- type: String,
50
- default: 'asc'
51
- },
52
- emptySlots: {
53
- type: String,
54
- default: 'show'
55
- },
56
- summary: {
57
- type: String,
58
- default: 'full'
59
- },
60
- primaryColor: String,
61
- secondaryColor: String,
62
- tertiaryColor: String
63
- });
64
-
65
- const document = inject('rawDocument');
66
-
67
- // Calculate grid configuration
68
- const gridConfig = computed(() => {
69
- const columns = props.columns || 5;
70
- const rows = props.rows || 3;
71
-
72
- // Calculate total slots from columns × rows
73
- let totalSlots = columns * rows;
74
-
75
- // Cap at maxSlots if specified
76
- if (props.maxSlots) {
77
- totalSlots = Math.min(totalSlots, props.maxSlots);
78
- }
79
-
80
- return { totalSlots, columns };
81
- });
82
-
83
- // Force update trigger for reactive updates
84
- const updateKey = ref(0);
85
- const forceUpdate = () => {
86
- updateKey.value++;
87
- };
88
-
89
- // Get filtered items
90
- const filteredItems = computed(() => {
91
- // Depend on updateKey to trigger re-evaluation
92
- const _ = updateKey.value;
93
- let items = Array.from(document.items || []);
94
-
95
- // Filter by document type
96
- items = items.filter(item => item.type === props.documentType);
97
-
98
- // Apply where expression if provided
99
- if (props.whereExpression) {
100
- try {
101
- const filterFunc = new Function('item', 'system', \`return \${props.whereExpression}\`);
102
- items = items.filter(item => filterFunc(item, props.context.system));
103
- } catch (e) {
104
- console.error('Error filtering inventory items:', e);
105
- }
106
- }
107
-
108
- // Add global items if allowed
109
- if (props.globalAllowed) {
110
- const gameItems = game.items.filter(item => item.type === props.documentType);
111
- items = items.concat(gameItems);
112
-
113
- const itemPacks = game.packs.filter(pack => pack.documentName === 'Item');
114
- for (let pack of itemPacks) {
115
- const packItems = pack.index.contents.filter(item => item.type === props.documentType);
116
- packItems.forEach(item => {
117
- item.compendium = pack;
118
- });
119
- items = items.concat(packItems);
120
- }
121
- }
122
-
123
- // Apply sorting if specified
124
- if (props.sortProperty) {
125
- items.sort((a, b) => {
126
- const aProp = foundry.utils.getProperty(a, \`system.\${props.sortProperty.toLowerCase()}\`);
127
- const bProp = foundry.utils.getProperty(b, \`system.\${props.sortProperty.toLowerCase()}\`);
128
-
129
- let aVal = aProp;
130
- let bVal = bProp;
131
-
132
- // Handle nested values (like resource.value)
133
- if (typeof aProp === 'object' && aProp?.value !== undefined) aVal = aProp.value;
134
- if (typeof bProp === 'object' && bProp?.value !== undefined) bVal = bProp.value;
135
-
136
- if (aVal < bVal) return props.sortOrder === 'asc' ? -1 : 1;
137
- if (aVal > bVal) return props.sortOrder === 'asc' ? 1 : -1;
138
- return 0;
139
- });
140
- }
141
-
142
- return items;
143
- });
144
-
145
- // Generate slots for display
146
- const inventorySlots = computed(() => {
147
- const slots = [];
148
- const config = gridConfig.value;
149
- let firstEmptyFound = false;
150
-
151
- for (let i = 0; i < config.totalSlots; i++) {
152
- if (i < filteredItems.value.length) {
153
- const item = filteredItems.value[i];
154
- let quantity = null;
155
-
156
- if (props.quantityField) {
157
- const quantityValue = foundry.utils.getProperty(item, \`system.\${props.quantityField.toLowerCase()}\`);
158
- quantity = typeof quantityValue === 'object' ? quantityValue.value : quantityValue;
159
- }
160
-
161
- slots.push({
162
- item: item,
163
- quantity: quantity,
164
- empty: false,
165
- isCreateSlot: false
166
- });
167
- } else if (props.emptySlots === 'show') {
168
- const isCreateSlot = !firstEmptyFound;
169
- if (isCreateSlot) firstEmptyFound = true;
170
-
171
- slots.push({
172
- item: null,
173
- quantity: null,
174
- empty: true,
175
- isCreateSlot: isCreateSlot
176
- });
177
- }
178
- }
179
-
180
- return slots;
181
- });
182
-
183
- // Calculate aggregations
184
- const aggregations = computed(() => {
185
- if (!props.sumProperties || props.sumProperties.length === 0) {
186
- return [];
187
- }
188
-
189
- return props.sumProperties.map((propName, index) => {
190
- let total = 0;
191
-
192
- filteredItems.value.forEach(item => {
193
- const propValue = foundry.utils.getProperty(item, \`system.\${propName.toLowerCase()}\`);
194
-
195
- if (typeof propValue === 'number') {
196
- total += propValue;
197
- } else if (propValue?.value !== undefined) {
198
- total += propValue.value;
199
- }
200
- });
201
-
202
- const max = props.sumMax && props.sumMax[index] ? props.sumMax[index] : null;
203
-
204
- return {
205
- name: propName,
206
- label: game.i18n.localize(propName),
207
- value: total,
208
- formatted: total.toLocaleString(),
209
- max: max,
210
- percentage: max ? Math.min(100, (total / max) * 100) : null
211
- };
212
- });
213
- });
214
-
215
- // Get money value
216
- const moneyValue = computed(() => {
217
- // Depend on updateKey to trigger re-evaluation
218
- const _ = updateKey.value;
219
- if (!props.moneyField) return null;
220
- return foundry.utils.getProperty(document, \`system.\${props.moneyField.toLowerCase()}\`);
221
- });
222
-
223
- // Format money display
224
- const formattedMoney = computed(() => {
225
- if (!moneyValue.value) return '0';
226
-
227
- if (typeof moneyValue.value === 'number') {
228
- return moneyValue.value.toLocaleString();
229
- }
230
-
231
- // Multi-denomination money
232
- if (typeof moneyValue.value === 'object') {
233
- const parts = [];
234
- for (const [denom, amount] of Object.entries(moneyValue.value)) {
235
- if (amount > 0) {
236
- parts.push(\`\${amount}\${denom.charAt(0)}\`);
237
- }
238
- }
239
- return parts.length > 0 ? parts.join(' ') : '0';
240
- }
241
-
242
- return '0';
243
- });
244
-
245
- // Get capacity color for a percentage
246
- const getCapacityColor = (percentage) => {
247
- if (!percentage) return 'success';
248
- if (percentage > 100) return 'error';
249
- if (percentage >= 90) return 'error';
250
- if (percentage >= 70) return 'warning';
251
- return 'success';
252
- };
253
-
254
- // Handle item click
255
- const onItemClick = (slot) => {
256
- if (slot.empty) {
257
- if (slot.isCreateSlot) {
258
- onCreateItem();
259
- }
260
- return;
261
- }
262
- if (slot.item) {
263
- slot.item.sheet.render(true);
264
- }
265
- };
266
-
267
- // Handle create new item
268
- const onCreateItem = async () => {
269
- if (!props.documentType) return;
270
-
271
- const itemData = {
272
- name: game.i18n.localize(\`New \${props.documentType}\`),
273
- type: props.documentType
274
- };
275
-
276
- const created = await document.createEmbeddedDocuments('Item', [itemData]);
277
- if (created && created[0]) {
278
- created[0].sheet.render(true);
279
- }
280
- };
281
-
282
- // Handle delete item
283
- const onDeleteItem = async (event, slot) => {
284
- // Prevent the click from opening the item sheet
285
- event.stopPropagation();
286
-
287
- if (!slot.item) return;
288
-
289
- const confirmed = await Dialog.confirm({
290
- title: game.i18n.localize('Delete Item'),
291
- content: \`<p>\${game.i18n.format('Are you sure you want to delete {name}?', { name: slot.item.name })}</p>\`,
292
- });
293
-
294
- if (confirmed) {
295
- await slot.item.delete();
296
- }
297
- };
298
-
299
- // Handle drag start
300
- const onDragStart = (event, slot) => {
301
- if (!slot.item) return;
302
-
303
- const dragData = {
304
- type: 'Item',
305
- uuid: slot.item.uuid
306
- };
307
-
308
- event.dataTransfer.setData('text/plain', JSON.stringify(dragData));
309
-
310
- // Add dragging class for visual feedback
311
- event.target.classList.add('dragging');
312
- };
313
-
314
- // Handle drag end
315
- const onDragEnd = (event) => {
316
- // Remove dragging class
317
- event.target.classList.remove('dragging');
318
- };
319
-
320
- // Get item name for tooltip
321
- const getItemName = (slot) => {
322
- if (slot.empty || !slot.item) return '';
323
- return slot.item.name;
324
- };
325
-
326
- // Get item description for tooltip
327
- const getItemDescription = (slot) => {
328
- if (slot.empty || !slot.item) return '';
329
-
330
- // Get description - handle both value and direct description
331
- const description = slot.item.system?.description?.value || slot.item.system?.description;
332
- if (!description) return '';
333
-
334
- // Truncate if too long
335
- if (description.length > 200) {
336
- return description.substring(0, 200) + '...';
337
- }
338
-
339
- return description;
340
- };
341
-
342
- // Get summed property values for tooltip
343
- const getItemSumProperties = (slot) => {
344
- if (slot.empty || !slot.item || !props.sumProperties || props.sumProperties.length === 0) {
345
- return [];
346
- }
347
-
348
- return props.sumProperties.map(propName => {
349
- const propValue = foundry.utils.getProperty(slot.item, \`system.\${propName.toLowerCase()}\`);
350
-
351
- let value = 0;
352
- if (typeof propValue === 'number') {
353
- value = propValue;
354
- } else if (propValue?.value !== undefined) {
355
- value = propValue.value;
356
- }
357
-
358
- return {
359
- name: propName,
360
- label: game.i18n.localize(propName),
361
- value: value,
362
- formatted: value.toLocaleString()
363
- };
364
- }).filter(prop => prop.value > 0); // Only show properties with values
365
- };
366
-
367
- // Count items
368
- const itemCount = computed(() => {
369
- return filteredItems.value.length;
370
- });
371
-
372
- // Show summary based on mode
373
- const showCount = computed(() => props.summary !== 'minimal');
374
- const showAggregations = computed(() => props.summary === 'full');
375
- const showMoney = computed(() => props.summary === 'full' && props.moneyField);
376
-
377
- // Subscribe to item changes
378
- const onItemChange = (item, options, userId) => {
379
- // Check if this item belongs to our document
380
- if (item.parent?.uuid === document.uuid) {
381
- forceUpdate();
382
- }
383
- };
384
-
385
- onMounted(() => {
386
- Hooks.on('createItem', onItemChange);
387
- Hooks.on('updateItem', onItemChange);
388
- Hooks.on('deleteItem', onItemChange);
389
- });
390
-
391
- onUnmounted(() => {
392
- Hooks.off('createItem', onItemChange);
393
- Hooks.off('updateItem', onItemChange);
394
- Hooks.off('deleteItem', onItemChange);
395
- });
396
- </script>
397
-
398
- <template>
399
- <div class="isdl-inventory" :key="updateKey">
400
- <!-- Header -->
401
- <div class="inventory-header" v-if="showCount">
402
- <div class="header-content">
403
- <v-icon v-if="icon" :icon="icon" size="small"></v-icon>
404
- <span class="inventory-label">{{ game.i18n.localize(label) }}</span>
405
- </div>
406
- <span class="inventory-count">
407
- {{ itemCount }}/{{ gridConfig.totalSlots }}
408
- </span>
409
- </div>
410
-
411
- <!-- Grid Container -->
412
- <div class="inventory-grid-container">
413
- <div
414
- class="inventory-grid"
415
- :style="{
416
- gridTemplateColumns: \`repeat(\${gridConfig.columns}, \${slotSize}px)\`,
417
- }"
418
- >
419
- <v-tooltip
420
- v-for="(slot, index) in inventorySlots"
421
- :key="index"
422
- :disabled="slot.empty && !slot.isCreateSlot"
423
- location="top"
424
- max-width="400"
425
- >
426
- <template v-slot:activator="{ props: tooltipProps }">
427
- <div
428
- v-bind="tooltipProps"
429
- class="inventory-slot"
430
- :class="{
431
- 'empty': slot.empty,
432
- 'filled': !slot.empty,
433
- 'create-slot': slot.isCreateSlot
434
- }"
435
- :style="{
436
- width: \`\${slotSize}px\`,
437
- height: \`\${slotSize}px\`
438
- }"
439
- :draggable="!slot.empty && !!slot.item"
440
- @click="onItemClick(slot)"
441
- @dragstart="onDragStart($event, slot)"
442
- @dragend="onDragEnd($event)"
443
- >
444
- <img
445
- v-if="!slot.empty && slot.item"
446
- :src="slot.item.img"
447
- :alt="slot.item.name"
448
- class="slot-image"
449
- />
450
- <v-icon
451
- v-if="slot.isCreateSlot"
452
- icon="fa-solid fa-plus"
453
- size="large"
454
- class="create-icon"
455
- ></v-icon>
456
- <div
457
- v-if="!slot.empty && slot.item"
458
- class="delete-button"
459
- @click="onDeleteItem($event, slot)"
460
- :data-tooltip="game.i18n.localize('Delete')"
461
- >
462
- <v-icon icon="fa-solid fa-times" size="x-small"></v-icon>
463
- </div>
464
- <div
465
- v-if="!slot.empty && slot.quantity && slot.quantity > 1"
466
- class="quantity-badge"
467
- >
468
- {{ slot.quantity }}
469
- </div>
470
- </div>
471
- </template>
472
- <template v-slot:default>
473
- <div v-if="slot.isCreateSlot" class="inventory-tooltip">
474
- <div class="tooltip-title">{{ game.i18n.localize('Create New Item') }}</div>
475
- </div>
476
- <div v-else class="inventory-tooltip">
477
- <div class="tooltip-title">{{ getItemName(slot) }}</div>
478
- <div v-if="getItemDescription(slot)" class="tooltip-description" v-html="getItemDescription(slot)"></div>
479
- <div v-if="getItemSumProperties(slot).length > 0" class="tooltip-properties">
480
- <div v-for="prop in getItemSumProperties(slot)" :key="prop.name" class="tooltip-property">
481
- <span class="property-label">{{ prop.label }}:</span>
482
- <span class="property-value">{{ prop.formatted }}</span>
483
- </div>
484
- </div>
485
- </div>
486
- </template>
487
- </v-tooltip>
488
- </div>
489
- </div>
490
-
491
- <!-- Footer -->
492
- <div class="inventory-footer" v-if="showAggregations || showMoney">
493
- <v-divider class="footer-divider"></v-divider>
494
-
495
- <!-- Money Display -->
496
- <div v-if="showMoney" class="inventory-stat money-display">
497
- <div class="stat-header">
498
- <v-icon v-if="moneyFieldIcon" :icon="moneyFieldIcon" size="small"></v-icon>
499
- <span class="stat-label">{{ game.i18n.localize(moneyFieldLabel || 'Currency') }}</span>
500
- </div>
501
- <span class="stat-value">{{ formattedMoney }}</span>
502
- </div>
503
-
504
- <!-- Aggregations -->
505
- <div v-if="showAggregations" v-for="agg in aggregations" :key="agg.name" class="inventory-stat aggregation-display">
506
- <div class="stat-header">
507
- <v-icon icon="fa-solid fa-calculator" size="small"></v-icon>
508
- <span class="stat-label">{{ agg.label }}</span>
509
- </div>
510
- <div class="stat-value-container">
511
- <span class="stat-value">
512
- {{ agg.formatted }}
513
- <span v-if="agg.max" class="stat-max"> / {{ agg.max }}</span>
514
- </span>
515
- <!-- Progress bar for aggregations with max -->
516
- <v-progress-linear
517
- v-if="agg.max"
518
- :model-value="agg.percentage"
519
- :color="getCapacityColor(agg.percentage)"
520
- height="6"
521
- rounded
522
- class="capacity-progress"
523
- ></v-progress-linear>
524
- </div>
525
- </div>
526
- </div>
527
- </div>
528
- </template>
10
+ const fileNode = expandToNode `
11
+ <script setup>
12
+ import { ref, computed, inject, onMounted, onUnmounted } from "vue";
13
+
14
+ const props = defineProps({
15
+ label: String,
16
+ systemPath: String,
17
+ context: Object,
18
+ editMode: Boolean,
19
+ icon: String,
20
+ color: String,
21
+ disabled: Boolean,
22
+ maxSlots: {
23
+ type: Number,
24
+ default: 20
25
+ },
26
+ columns: {
27
+ type: Number,
28
+ default: 5
29
+ },
30
+ rows: {
31
+ type: Number,
32
+ default: 3
33
+ },
34
+ slotSize: {
35
+ type: Number,
36
+ default: 60
37
+ },
38
+ documentType: String,
39
+ whereExpression: String,
40
+ globalAllowed: Boolean,
41
+ quantityField: String,
42
+ moneyField: String,
43
+ moneyFieldLabel: String,
44
+ moneyFieldIcon: String,
45
+ sumProperties: Array,
46
+ sumMax: Array,
47
+ sortProperty: String,
48
+ sortOrder: {
49
+ type: String,
50
+ default: 'asc'
51
+ },
52
+ emptySlots: {
53
+ type: String,
54
+ default: 'show'
55
+ },
56
+ summary: {
57
+ type: String,
58
+ default: 'full'
59
+ },
60
+ primaryColor: String,
61
+ secondaryColor: String,
62
+ tertiaryColor: String
63
+ });
64
+
65
+ const document = inject('rawDocument');
66
+
67
+ // Calculate grid configuration
68
+ const gridConfig = computed(() => {
69
+ const columns = props.columns || 5;
70
+ const rows = props.rows || 3;
71
+
72
+ // Calculate total slots from columns × rows
73
+ let totalSlots = columns * rows;
74
+
75
+ // Cap at maxSlots if specified
76
+ if (props.maxSlots) {
77
+ totalSlots = Math.min(totalSlots, props.maxSlots);
78
+ }
79
+
80
+ return { totalSlots, columns };
81
+ });
82
+
83
+ // Force update trigger for reactive updates
84
+ const updateKey = ref(0);
85
+ const forceUpdate = () => {
86
+ updateKey.value++;
87
+ };
88
+
89
+ // Get filtered items
90
+ const filteredItems = computed(() => {
91
+ // Depend on updateKey to trigger re-evaluation
92
+ const _ = updateKey.value;
93
+ let items = Array.from(document.items || []);
94
+
95
+ // Filter by document type
96
+ items = items.filter(item => item.type === props.documentType);
97
+
98
+ // Apply where expression if provided
99
+ if (props.whereExpression) {
100
+ try {
101
+ const filterFunc = new Function('item', 'system', \`return \${props.whereExpression}\`);
102
+ items = items.filter(item => filterFunc(item, props.context.system));
103
+ } catch (e) {
104
+ console.error('Error filtering inventory items:', e);
105
+ }
106
+ }
107
+
108
+ // Add global items if allowed
109
+ if (props.globalAllowed) {
110
+ const gameItems = game.items.filter(item => item.type === props.documentType);
111
+ items = items.concat(gameItems);
112
+
113
+ const itemPacks = game.packs.filter(pack => pack.documentName === 'Item');
114
+ for (let pack of itemPacks) {
115
+ const packItems = pack.index.contents.filter(item => item.type === props.documentType);
116
+ packItems.forEach(item => {
117
+ item.compendium = pack;
118
+ });
119
+ items = items.concat(packItems);
120
+ }
121
+ }
122
+
123
+ // Apply sorting if specified
124
+ if (props.sortProperty) {
125
+ items.sort((a, b) => {
126
+ const aProp = foundry.utils.getProperty(a, \`system.\${props.sortProperty.toLowerCase()}\`);
127
+ const bProp = foundry.utils.getProperty(b, \`system.\${props.sortProperty.toLowerCase()}\`);
128
+
129
+ let aVal = aProp;
130
+ let bVal = bProp;
131
+
132
+ // Handle nested values (like resource.value)
133
+ if (typeof aProp === 'object' && aProp?.value !== undefined) aVal = aProp.value;
134
+ if (typeof bProp === 'object' && bProp?.value !== undefined) bVal = bProp.value;
135
+
136
+ if (aVal < bVal) return props.sortOrder === 'asc' ? -1 : 1;
137
+ if (aVal > bVal) return props.sortOrder === 'asc' ? 1 : -1;
138
+ return 0;
139
+ });
140
+ }
141
+
142
+ return items;
143
+ });
144
+
145
+ // Generate slots for display
146
+ const inventorySlots = computed(() => {
147
+ const slots = [];
148
+ const config = gridConfig.value;
149
+ let firstEmptyFound = false;
150
+
151
+ for (let i = 0; i < config.totalSlots; i++) {
152
+ if (i < filteredItems.value.length) {
153
+ const item = filteredItems.value[i];
154
+ let quantity = null;
155
+
156
+ if (props.quantityField) {
157
+ const quantityValue = foundry.utils.getProperty(item, \`system.\${props.quantityField.toLowerCase()}\`);
158
+ quantity = typeof quantityValue === 'object' ? quantityValue.value : quantityValue;
159
+ }
160
+
161
+ slots.push({
162
+ item: item,
163
+ quantity: quantity,
164
+ empty: false,
165
+ isCreateSlot: false
166
+ });
167
+ } else if (props.emptySlots === 'show') {
168
+ const isCreateSlot = !firstEmptyFound;
169
+ if (isCreateSlot) firstEmptyFound = true;
170
+
171
+ slots.push({
172
+ item: null,
173
+ quantity: null,
174
+ empty: true,
175
+ isCreateSlot: isCreateSlot
176
+ });
177
+ }
178
+ }
179
+
180
+ return slots;
181
+ });
182
+
183
+ // Calculate aggregations
184
+ const aggregations = computed(() => {
185
+ if (!props.sumProperties || props.sumProperties.length === 0) {
186
+ return [];
187
+ }
188
+
189
+ return props.sumProperties.map((propName, index) => {
190
+ let total = 0;
191
+
192
+ filteredItems.value.forEach(item => {
193
+ const propValue = foundry.utils.getProperty(item, \`system.\${propName.toLowerCase()}\`);
194
+
195
+ if (typeof propValue === 'number') {
196
+ total += propValue;
197
+ } else if (propValue?.value !== undefined) {
198
+ total += propValue.value;
199
+ }
200
+ });
201
+
202
+ const max = props.sumMax && props.sumMax[index] ? props.sumMax[index] : null;
203
+
204
+ return {
205
+ name: propName,
206
+ label: game.i18n.localize(propName),
207
+ value: total,
208
+ formatted: total.toLocaleString(),
209
+ max: max,
210
+ percentage: max ? Math.min(100, (total / max) * 100) : null
211
+ };
212
+ });
213
+ });
214
+
215
+ // Get money value
216
+ const moneyValue = computed(() => {
217
+ // Depend on updateKey to trigger re-evaluation
218
+ const _ = updateKey.value;
219
+ if (!props.moneyField) return null;
220
+ return foundry.utils.getProperty(document, \`system.\${props.moneyField.toLowerCase()}\`);
221
+ });
222
+
223
+ // Format money display
224
+ const formattedMoney = computed(() => {
225
+ if (!moneyValue.value) return '0';
226
+
227
+ if (typeof moneyValue.value === 'number') {
228
+ return moneyValue.value.toLocaleString();
229
+ }
230
+
231
+ // Multi-denomination money
232
+ if (typeof moneyValue.value === 'object') {
233
+ const parts = [];
234
+ for (const [denom, amount] of Object.entries(moneyValue.value)) {
235
+ if (amount > 0) {
236
+ parts.push(\`\${amount}\${denom.charAt(0)}\`);
237
+ }
238
+ }
239
+ return parts.length > 0 ? parts.join(' ') : '0';
240
+ }
241
+
242
+ return '0';
243
+ });
244
+
245
+ // Get capacity color for a percentage
246
+ const getCapacityColor = (percentage) => {
247
+ if (!percentage) return 'success';
248
+ if (percentage > 100) return 'error';
249
+ if (percentage >= 90) return 'error';
250
+ if (percentage >= 70) return 'warning';
251
+ return 'success';
252
+ };
253
+
254
+ // Handle item click
255
+ const onItemClick = (slot) => {
256
+ if (slot.empty) {
257
+ if (slot.isCreateSlot) {
258
+ onCreateItem();
259
+ }
260
+ return;
261
+ }
262
+ if (slot.item) {
263
+ slot.item.sheet.render(true);
264
+ }
265
+ };
266
+
267
+ // Handle create new item
268
+ const onCreateItem = async () => {
269
+ if (!props.documentType) return;
270
+
271
+ const itemData = {
272
+ name: game.i18n.localize(\`New \${props.documentType}\`),
273
+ type: props.documentType
274
+ };
275
+
276
+ const created = await document.createEmbeddedDocuments('Item', [itemData]);
277
+ if (created && created[0]) {
278
+ created[0].sheet.render(true);
279
+ }
280
+ };
281
+
282
+ // Handle delete item
283
+ const onDeleteItem = async (event, slot) => {
284
+ // Prevent the click from opening the item sheet
285
+ event.stopPropagation();
286
+
287
+ if (!slot.item) return;
288
+
289
+ const confirmed = await Dialog.confirm({
290
+ title: game.i18n.localize('Delete Item'),
291
+ content: \`<p>\${game.i18n.format('Are you sure you want to delete {name}?', { name: slot.item.name })}</p>\`,
292
+ });
293
+
294
+ if (confirmed) {
295
+ await slot.item.delete();
296
+ }
297
+ };
298
+
299
+ // Handle drag start
300
+ const onDragStart = (event, slot) => {
301
+ if (!slot.item) return;
302
+
303
+ const dragData = {
304
+ type: 'Item',
305
+ uuid: slot.item.uuid
306
+ };
307
+
308
+ event.dataTransfer.setData('text/plain', JSON.stringify(dragData));
309
+
310
+ // Add dragging class for visual feedback
311
+ event.target.classList.add('dragging');
312
+ };
313
+
314
+ // Handle drag end
315
+ const onDragEnd = (event) => {
316
+ // Remove dragging class
317
+ event.target.classList.remove('dragging');
318
+ };
319
+
320
+ // Get item name for tooltip
321
+ const getItemName = (slot) => {
322
+ if (slot.empty || !slot.item) return '';
323
+ return slot.item.name;
324
+ };
325
+
326
+ // Get item description for tooltip
327
+ const getItemDescription = (slot) => {
328
+ if (slot.empty || !slot.item) return '';
329
+
330
+ // Get description - handle both value and direct description
331
+ const description = slot.item.system?.description?.value || slot.item.system?.description;
332
+ if (!description) return '';
333
+
334
+ // Truncate if too long
335
+ if (description.length > 200) {
336
+ return description.substring(0, 200) + '...';
337
+ }
338
+
339
+ return description;
340
+ };
341
+
342
+ // Get summed property values for tooltip
343
+ const getItemSumProperties = (slot) => {
344
+ if (slot.empty || !slot.item || !props.sumProperties || props.sumProperties.length === 0) {
345
+ return [];
346
+ }
347
+
348
+ return props.sumProperties.map(propName => {
349
+ const propValue = foundry.utils.getProperty(slot.item, \`system.\${propName.toLowerCase()}\`);
350
+
351
+ let value = 0;
352
+ if (typeof propValue === 'number') {
353
+ value = propValue;
354
+ } else if (propValue?.value !== undefined) {
355
+ value = propValue.value;
356
+ }
357
+
358
+ return {
359
+ name: propName,
360
+ label: game.i18n.localize(propName),
361
+ value: value,
362
+ formatted: value.toLocaleString()
363
+ };
364
+ }).filter(prop => prop.value > 0); // Only show properties with values
365
+ };
366
+
367
+ // Count items
368
+ const itemCount = computed(() => {
369
+ return filteredItems.value.length;
370
+ });
371
+
372
+ // Show summary based on mode
373
+ const showCount = computed(() => props.summary !== 'minimal');
374
+ const showAggregations = computed(() => props.summary === 'full');
375
+ const showMoney = computed(() => props.summary === 'full' && props.moneyField);
376
+
377
+ // Subscribe to item changes
378
+ const onItemChange = (item, options, userId) => {
379
+ // Check if this item belongs to our document
380
+ if (item.parent?.uuid === document.uuid) {
381
+ forceUpdate();
382
+ }
383
+ };
384
+
385
+ onMounted(() => {
386
+ Hooks.on('createItem', onItemChange);
387
+ Hooks.on('updateItem', onItemChange);
388
+ Hooks.on('deleteItem', onItemChange);
389
+ });
390
+
391
+ onUnmounted(() => {
392
+ Hooks.off('createItem', onItemChange);
393
+ Hooks.off('updateItem', onItemChange);
394
+ Hooks.off('deleteItem', onItemChange);
395
+ });
396
+ </script>
397
+
398
+ <template>
399
+ <div class="isdl-inventory" :key="updateKey">
400
+ <!-- Header -->
401
+ <div class="inventory-header" v-if="showCount">
402
+ <div class="header-content">
403
+ <v-icon v-if="icon" :icon="icon" size="small"></v-icon>
404
+ <span class="inventory-label">{{ game.i18n.localize(label) }}</span>
405
+ </div>
406
+ <span class="inventory-count">
407
+ {{ itemCount }}/{{ gridConfig.totalSlots }}
408
+ </span>
409
+ </div>
410
+
411
+ <!-- Grid Container -->
412
+ <div class="inventory-grid-container">
413
+ <div
414
+ class="inventory-grid"
415
+ :style="{
416
+ gridTemplateColumns: \`repeat(\${gridConfig.columns}, \${slotSize}px)\`,
417
+ }"
418
+ >
419
+ <v-tooltip
420
+ v-for="(slot, index) in inventorySlots"
421
+ :key="index"
422
+ :disabled="slot.empty && !slot.isCreateSlot"
423
+ location="top"
424
+ max-width="400"
425
+ >
426
+ <template v-slot:activator="{ props: tooltipProps }">
427
+ <div
428
+ v-bind="tooltipProps"
429
+ class="inventory-slot"
430
+ :class="{
431
+ 'empty': slot.empty,
432
+ 'filled': !slot.empty,
433
+ 'create-slot': slot.isCreateSlot
434
+ }"
435
+ :style="{
436
+ width: \`\${slotSize}px\`,
437
+ height: \`\${slotSize}px\`
438
+ }"
439
+ :draggable="!slot.empty && !!slot.item"
440
+ @click="onItemClick(slot)"
441
+ @dragstart="onDragStart($event, slot)"
442
+ @dragend="onDragEnd($event)"
443
+ >
444
+ <img
445
+ v-if="!slot.empty && slot.item"
446
+ :src="slot.item.img"
447
+ :alt="slot.item.name"
448
+ class="slot-image"
449
+ />
450
+ <v-icon
451
+ v-if="slot.isCreateSlot"
452
+ icon="fa-solid fa-plus"
453
+ size="large"
454
+ class="create-icon"
455
+ ></v-icon>
456
+ <div
457
+ v-if="!slot.empty && slot.item"
458
+ class="delete-button"
459
+ @click="onDeleteItem($event, slot)"
460
+ :data-tooltip="game.i18n.localize('Delete')"
461
+ >
462
+ <v-icon icon="fa-solid fa-times" size="x-small"></v-icon>
463
+ </div>
464
+ <div
465
+ v-if="!slot.empty && slot.quantity && slot.quantity > 1"
466
+ class="quantity-badge"
467
+ >
468
+ {{ slot.quantity }}
469
+ </div>
470
+ </div>
471
+ </template>
472
+ <template v-slot:default>
473
+ <div v-if="slot.isCreateSlot" class="inventory-tooltip">
474
+ <div class="tooltip-title">{{ game.i18n.localize('Create New Item') }}</div>
475
+ </div>
476
+ <div v-else class="inventory-tooltip">
477
+ <div class="tooltip-title">{{ getItemName(slot) }}</div>
478
+ <div v-if="getItemDescription(slot)" class="tooltip-description" v-html="getItemDescription(slot)"></div>
479
+ <div v-if="getItemSumProperties(slot).length > 0" class="tooltip-properties">
480
+ <div v-for="prop in getItemSumProperties(slot)" :key="prop.name" class="tooltip-property">
481
+ <span class="property-label">{{ prop.label }}:</span>
482
+ <span class="property-value">{{ prop.formatted }}</span>
483
+ </div>
484
+ </div>
485
+ </div>
486
+ </template>
487
+ </v-tooltip>
488
+ </div>
489
+ </div>
490
+
491
+ <!-- Footer -->
492
+ <div class="inventory-footer" v-if="showAggregations || showMoney">
493
+ <v-divider class="footer-divider"></v-divider>
494
+
495
+ <!-- Money Display -->
496
+ <div v-if="showMoney" class="inventory-stat money-display">
497
+ <div class="stat-header">
498
+ <v-icon v-if="moneyFieldIcon" :icon="moneyFieldIcon" size="small"></v-icon>
499
+ <span class="stat-label">{{ game.i18n.localize(moneyFieldLabel || 'Currency') }}</span>
500
+ </div>
501
+ <span class="stat-value">{{ formattedMoney }}</span>
502
+ </div>
503
+
504
+ <!-- Aggregations -->
505
+ <div v-if="showAggregations" v-for="agg in aggregations" :key="agg.name" class="inventory-stat aggregation-display">
506
+ <div class="stat-header">
507
+ <v-icon icon="fa-solid fa-calculator" size="small"></v-icon>
508
+ <span class="stat-label">{{ agg.label }}</span>
509
+ </div>
510
+ <div class="stat-value-container">
511
+ <span class="stat-value">
512
+ {{ agg.formatted }}
513
+ <span v-if="agg.max" class="stat-max"> / {{ agg.max }}</span>
514
+ </span>
515
+ <!-- Progress bar for aggregations with max -->
516
+ <v-progress-linear
517
+ v-if="agg.max"
518
+ :model-value="agg.percentage"
519
+ :color="getCapacityColor(agg.percentage)"
520
+ height="6"
521
+ rounded
522
+ class="capacity-progress"
523
+ ></v-progress-linear>
524
+ </div>
525
+ </div>
526
+ </div>
527
+ </div>
528
+ </template>
529
529
  `.appendNewLine();
530
530
  fs.writeFileSync(generatedFilePath, toString(fileNode));
531
531
  }