intelligent-system-design-language 0.3.22 → 0.3.24

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 (106) 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 -1444
  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 -1444
  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 -654
  27. package/out/cli/components/css-generator.js +4 -4
  28. package/out/cli/components/damage-roll-generator.js +160 -160
  29. package/out/cli/components/datamodel-generator.js +257 -257
  30. package/out/cli/components/derived-data-generator.js +923 -923
  31. package/out/cli/components/hotbar-drop-hook-generator.js +82 -82
  32. package/out/cli/components/init-hook-generator.js +495 -495
  33. package/out/cli/components/language-generator.js +1 -1
  34. package/out/cli/components/language-generator.js.map +1 -1
  35. package/out/cli/components/measured-template-preview.js +221 -221
  36. package/out/cli/components/method-generator.js +601 -562
  37. package/out/cli/components/method-generator.js.map +1 -1
  38. package/out/cli/components/ready-hook-generator.js +404 -404
  39. package/out/cli/components/token-generator.js +116 -116
  40. package/out/cli/components/vue/base-components/vue-attribute.js +138 -138
  41. package/out/cli/components/vue/base-components/vue-boolean.js +64 -64
  42. package/out/cli/components/vue/base-components/vue-calculator.js +93 -93
  43. package/out/cli/components/vue/base-components/vue-damage-application.js +356 -356
  44. package/out/cli/components/vue/base-components/vue-damage-bonuses.js +165 -165
  45. package/out/cli/components/vue/base-components/vue-damage-resistances.js +196 -196
  46. package/out/cli/components/vue/base-components/vue-damage-track.js +121 -121
  47. package/out/cli/components/vue/base-components/vue-date-time.js +42 -42
  48. package/out/cli/components/vue/base-components/vue-dice.js +98 -98
  49. package/out/cli/components/vue/base-components/vue-die.js +73 -73
  50. package/out/cli/components/vue/base-components/vue-document-choice.js +149 -149
  51. package/out/cli/components/vue/base-components/vue-document-choices.js +179 -179
  52. package/out/cli/components/vue/base-components/vue-document-link.js +60 -60
  53. package/out/cli/components/vue/base-components/vue-extended-choice.js +88 -88
  54. package/out/cli/components/vue/base-components/vue-inventory.js +519 -519
  55. package/out/cli/components/vue/base-components/vue-macro-choice.js +138 -138
  56. package/out/cli/components/vue/base-components/vue-measured-template.js +530 -530
  57. package/out/cli/components/vue/base-components/vue-money.js +483 -483
  58. package/out/cli/components/vue/base-components/vue-number.js +174 -174
  59. package/out/cli/components/vue/base-components/vue-paperdoll.js +43 -43
  60. package/out/cli/components/vue/base-components/vue-parent-property-reference.js +76 -76
  61. package/out/cli/components/vue/base-components/vue-prosemirror.js +18 -18
  62. package/out/cli/components/vue/base-components/vue-resource.js +136 -136
  63. package/out/cli/components/vue/base-components/vue-roll-visualizer.js +285 -285
  64. package/out/cli/components/vue/base-components/vue-self-property-reference.js +62 -62
  65. package/out/cli/components/vue/base-components/vue-string-choice.js +98 -98
  66. package/out/cli/components/vue/base-components/vue-string-choices.js +203 -203
  67. package/out/cli/components/vue/base-components/vue-string.js +60 -60
  68. package/out/cli/components/vue/base-components/vue-text-field.js +53 -53
  69. package/out/cli/components/vue/base-components/vue-tracker.js +431 -431
  70. package/out/cli/components/vue/vue-action-component-generator.js +64 -64
  71. package/out/cli/components/vue/vue-active-effect-sheet-generator.js +856 -856
  72. package/out/cli/components/vue/vue-datatable-sheet-class-generator.js +292 -292
  73. package/out/cli/components/vue/vue-datatable2-component-generator.js +824 -824
  74. package/out/cli/components/vue/vue-document-creation-app.js +121 -121
  75. package/out/cli/components/vue/vue-document-creation-sheet.js +94 -94
  76. package/out/cli/components/vue/vue-generator.js +40 -40
  77. package/out/cli/components/vue/vue-mixin.js +296 -296
  78. package/out/cli/components/vue/vue-pinned-datatable-component-generator.js +260 -260
  79. package/out/cli/components/vue/vue-prompt-generator.js +80 -80
  80. package/out/cli/components/vue/vue-prompt-sheet-class-generator.js +317 -317
  81. package/out/cli/components/vue/vue-sheet-application-generator.js +1171 -1171
  82. package/out/cli/components/vue/vue-sheet-class-generator.js +510 -510
  83. package/out/cli/generator.js +508 -438
  84. package/out/cli/generator.js.map +1 -1
  85. package/out/extension/github/githubManager.js +35 -35
  86. package/out/extension/github/githubQuickActions.js +120 -120
  87. package/out/extension/github/system-workflow.yml +47 -47
  88. package/out/extension/main.cjs +881 -311
  89. package/out/extension/main.cjs.map +3 -3
  90. package/out/extension/package.json +419 -419
  91. package/out/language/generated/ast.js +98 -2
  92. package/out/language/generated/ast.js.map +1 -1
  93. package/out/language/generated/grammar.js +14688 -14240
  94. package/out/language/generated/grammar.js.map +1 -1
  95. package/out/language/intelligent-system-design-language-validator.js +44 -2
  96. package/out/language/intelligent-system-design-language-validator.js.map +1 -1
  97. package/out/language/isdl-scope-provider.js +7 -1
  98. package/out/language/isdl-scope-provider.js.map +1 -1
  99. package/out/language/main.cjs +881 -311
  100. package/out/language/main.cjs.map +3 -3
  101. package/out/package.json +419 -419
  102. package/out/progressbar.min.js +6 -6
  103. package/out/styles.scss +828 -762
  104. package/out/test/validating/diagnostics.test.js +55 -0
  105. package/out/test/validating/diagnostics.test.js.map +1 -1
  106. 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
  }