@raclettejs/core 0.1.18 → 0.1.19

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/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.19] - 12.12.2025 <a href="https://gitlab.com/raclettejs/core/-/compare/v0.1.18...v0.1.19" target="_blank" rel="noopener"><b>Overview of all changes</b></a>
11
+
12
+ ### Added
13
+
14
+ - Global: added bulk import to interactionLinks, compositions and tags
15
+
10
16
  ## [0.1.18] - 05.12.2025 <a href="https://gitlab.com/raclettejs/core/-/compare/v0.1.17...v0.1.18" target="_blank" rel="noopener"><b>Overview of all changes</b></a>
11
17
 
12
18
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raclettejs/core",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "description": "racletteJS core package",
5
5
  "repository": "https://gitlab.com/raclettejs/core",
6
6
  "author": "Pacifico Digital Explorations GmbH <info@raclettejs.com>",
@@ -69,7 +69,7 @@
69
69
  },
70
70
  "devDependencies": {
71
71
  "@eslint/js": "9.35.0",
72
- "@raclettejs/types": "^0.1.18",
72
+ "@raclettejs/types": "0.1.19",
73
73
  "@types/fs-extra": "11.0.4",
74
74
  "@types/js-yaml": "4.0.9",
75
75
  "@types/node": "24.5.1",
@@ -337,6 +337,90 @@ export class CompositionService {
337
337
  payload.body.deleted = [composition._id]
338
338
  fastify.emit("compositionHardDeleted", payload)
339
339
 
340
+ return payload
341
+ }
342
+ /**
343
+ * Core function to create multiple compositions (returns raw data)
344
+ */
345
+ async _createCompositions(
346
+ fastify: PluginFastifyInstance,
347
+ compositionBodies: CompositionBody[],
348
+ userId: string,
349
+ ): Promise<CompositionType[]> {
350
+ try {
351
+ const compositionsToCreate: CompositionBody[] = []
352
+
353
+ // Validate and prepare all compositions
354
+ for (const compositionBody of compositionBodies) {
355
+ if (compositionBody._id) {
356
+ const uuidValid = validate(compositionBody._id)
357
+
358
+ if (!uuidValid) {
359
+ throw new Error(
360
+ `Invalid ID - not a valid uuid v4: ${compositionBody._id}`,
361
+ )
362
+ }
363
+
364
+ const duplicate = await this.compositionModel.findOne({
365
+ _id: compositionBody._id,
366
+ })
367
+
368
+ if (duplicate) {
369
+ throw new Error(
370
+ `An entry with this id already exists: ${compositionBody._id}`,
371
+ )
372
+ }
373
+ } else {
374
+ compositionBody._id = uuidv4()
375
+ }
376
+
377
+ compositionsToCreate.push({
378
+ ...compositionBody,
379
+ author: userId,
380
+ lastEditor: userId,
381
+ })
382
+ }
383
+
384
+ // Bulk insert
385
+ const compositions =
386
+ await this.compositionModel.insertMany(compositionsToCreate)
387
+
388
+ fastify.log.info(
389
+ `[API] Created ${compositions.length} compositions in bulk`,
390
+ )
391
+
392
+ return compositions.map((comp) =>
393
+ comp.toObject ? comp.toObject() : comp,
394
+ )
395
+ } catch (error: any) {
396
+ fastify.log.error(error.message)
397
+ throw error
398
+ }
399
+ }
400
+
401
+ /**
402
+ * Create multiple compositions with payload wrapping and event emission
403
+ */
404
+ async createCompositions(
405
+ fastify: PluginFastifyInstance,
406
+ requestData: FrontendPayloadRequestData,
407
+ compositionBodies: CompositionBody[],
408
+ userId: string,
409
+ ): Promise<FrontendPayload<CompositionType[]>> {
410
+ const compositions = await this._createCompositions(
411
+ fastify,
412
+ compositionBodies,
413
+ userId,
414
+ )
415
+
416
+ const payload = await createCompositionPayload(
417
+ fastify,
418
+ compositions,
419
+ requestData,
420
+ )
421
+
422
+ fastify.emit("compositionsCreated", payload)
423
+
340
424
  return payload
341
425
  }
342
426
  }
@@ -4,6 +4,7 @@ import getAll from "./route.composition.getAll"
4
4
  import hardDelete from "./route.composition.hardDelete"
5
5
  import patch from "./route.composition.patch"
6
6
  import post from "./route.composition.post"
7
+ import postBulk from "./route.composition.post.bulk"
7
8
  import remove from "./route.composition.remove"
8
9
  import restore from "./route.composition.restore"
9
10
 
@@ -13,6 +14,7 @@ const registerRoutes = async (fastify: PluginFastifyInstance) => {
13
14
  await fastify.patch("/composition/:_id", patch(fastify))
14
15
  await fastify.patch("/composition/:_id/restore", restore(fastify))
15
16
  await fastify.post("/composition", post(fastify))
17
+ await fastify.post("/composition/bulk", postBulk(fastify))
16
18
  await fastify.delete("/composition/:_id", remove(fastify))
17
19
  await fastify.delete("/composition/:_id/hard", hardDelete(fastify))
18
20
  }
@@ -0,0 +1,58 @@
1
+ import type { CompositionBody } from "../composition.service"
2
+ import type { FastifyReply, FastifyRequest } from "fastify"
3
+ import type { PluginFastifyInstance } from "types"
4
+ import { compositionCreateSchema } from "../composition.schema"
5
+
6
+ export default (fastify: PluginFastifyInstance) => {
7
+ const compositionService = fastify.custom.compositionService
8
+
9
+ const handler = async (
10
+ req: FastifyRequest<{
11
+ Body: CompositionBody[]
12
+ }>,
13
+ reply: FastifyReply,
14
+ ) => {
15
+ try {
16
+ // Validate that body is an array
17
+ if (!Array.isArray(req.body)) {
18
+ return reply.badRequest("Request body must be an array of compositions")
19
+ }
20
+
21
+ // Validate that array is not empty
22
+ if (req.body.length === 0) {
23
+ return reply.badRequest("Request body cannot be an empty array")
24
+ }
25
+
26
+ const payload = await compositionService.createCompositions(
27
+ fastify,
28
+ req.requestParams,
29
+ req.body,
30
+ req.user._id,
31
+ )
32
+
33
+ return payload
34
+ } catch (error: any) {
35
+ fastify.log.error(error.message)
36
+
37
+ return reply.internalServerError(error.message)
38
+ }
39
+ }
40
+
41
+ return {
42
+ handler,
43
+ onRequest: [fastify.authenticate],
44
+ config: {
45
+ type: "dataCreate",
46
+ broadcastChannels: ["compositionsCreated"],
47
+ },
48
+ schema: {
49
+ summary: "Create multiple compositions in bulk",
50
+ tags: ["core/composition"],
51
+ body: {
52
+ type: "array",
53
+ items: compositionCreateSchema,
54
+ minItems: 1,
55
+ },
56
+ },
57
+ }
58
+ }
@@ -130,16 +130,16 @@ export class InteractionLinkService {
130
130
  }
131
131
 
132
132
  /**
133
- * Core function to get interaction links by composition ID (returns raw data)
133
+ * Core function to get interaction links by interactionLink ID (returns raw data)
134
134
  */
135
- async _readInteractionLinksByComposition(
135
+ async _readInteractionLinksByInteractionLink(
136
136
  fastify: PluginFastifyInstance,
137
- compositionId: string,
137
+ interactionLinkId: string,
138
138
  ): Promise<InteractionLinkType[]> {
139
139
  try {
140
140
  return await this.interactionLinkModel
141
141
  .find({
142
- composition: compositionId,
142
+ interactionLink: interactionLinkId,
143
143
  isDeleted: false,
144
144
  })
145
145
  .lean()
@@ -150,16 +150,16 @@ export class InteractionLinkService {
150
150
  }
151
151
 
152
152
  /**
153
- * Get interaction links by composition ID with payload wrapping
153
+ * Get interaction links by interactionLink ID with payload wrapping
154
154
  */
155
- async readInteractionLinksByComposition(
155
+ async readInteractionLinksByInteractionLink(
156
156
  fastify: PluginFastifyInstance,
157
157
  requestData: FrontendPayloadRequestData,
158
- compositionId: string,
158
+ interactionLinkId: string,
159
159
  ): Promise<FrontendPayload<InteractionLinkType[]>> {
160
- const interactionLinks = await this._readInteractionLinksByComposition(
160
+ const interactionLinks = await this._readInteractionLinksByInteractionLink(
161
161
  fastify,
162
- compositionId,
162
+ interactionLinkId,
163
163
  )
164
164
 
165
165
  return createInteractionLinkPayload(fastify, interactionLinks, requestData)
@@ -439,6 +439,91 @@ export class InteractionLinkService {
439
439
 
440
440
  return createInteractionLinkPayload(fastify, interactionLinks, requestData)
441
441
  }
442
+ /**
443
+ * Core function to create multiple interactionLinks (returns raw data)
444
+ */
445
+ async _createInteractionLinks(
446
+ fastify: PluginFastifyInstance,
447
+ interactionLinkBodies: InteractionLinkBody[],
448
+ userId: string,
449
+ ): Promise<InteractionLinkType[]> {
450
+ try {
451
+ const interactionLinksToCreate: InteractionLinkBody[] = []
452
+
453
+ // Validate and prepare all interactionLinks
454
+ for (const interactionLinkBody of interactionLinkBodies) {
455
+ if (interactionLinkBody._id) {
456
+ const uuidValid = validate(interactionLinkBody._id)
457
+
458
+ if (!uuidValid) {
459
+ throw new Error(
460
+ `Invalid ID - not a valid uuid v4: ${interactionLinkBody._id}`,
461
+ )
462
+ }
463
+
464
+ const duplicate = await this.interactionLinkModel.findOne({
465
+ _id: interactionLinkBody._id,
466
+ })
467
+
468
+ if (duplicate) {
469
+ throw new Error(
470
+ `An entry with this id already exists: ${interactionLinkBody._id}`,
471
+ )
472
+ }
473
+ } else {
474
+ interactionLinkBody._id = uuidv4()
475
+ }
476
+
477
+ interactionLinksToCreate.push({
478
+ ...interactionLinkBody,
479
+ author: userId,
480
+ lastEditor: userId,
481
+ })
482
+ }
483
+
484
+ // Bulk insert
485
+ const interactionLinks = await this.interactionLinkModel.insertMany(
486
+ interactionLinksToCreate,
487
+ )
488
+
489
+ fastify.log.info(
490
+ `[API] Created ${interactionLinks.length} interactionLinks in bulk`,
491
+ )
492
+
493
+ return interactionLinks.map((comp) =>
494
+ comp.toObject ? comp.toObject() : comp,
495
+ )
496
+ } catch (error: any) {
497
+ fastify.log.error(error.message)
498
+ throw error
499
+ }
500
+ }
501
+
502
+ /**
503
+ * Create multiple interactionLinks with payload wrapping and event emission
504
+ */
505
+ async createInteractionLinks(
506
+ fastify: PluginFastifyInstance,
507
+ requestData: FrontendPayloadRequestData,
508
+ interactionLinkBodies: InteractionLinkBody[],
509
+ userId: string,
510
+ ): Promise<FrontendPayload<InteractionLinkType[]>> {
511
+ const interactionLinks = await this._createInteractionLinks(
512
+ fastify,
513
+ interactionLinkBodies,
514
+ userId,
515
+ )
516
+
517
+ const payload = await createInteractionLinkPayload(
518
+ fastify,
519
+ interactionLinks,
520
+ requestData,
521
+ )
522
+
523
+ fastify.emit("interactionLinksCreated", payload)
524
+
525
+ return payload
526
+ }
442
527
  }
443
528
 
444
529
  export const createInteractionLinkService = (
@@ -4,6 +4,7 @@ import getAll from "./route.interactionLink.getAll"
4
4
  import hardDelete from "./route.interactionLink.hardDelete"
5
5
  import patch from "./route.interactionLink.patch"
6
6
  import post from "./route.interactionLink.post"
7
+ import postBulk from "./route.interactionLink.post.bulk"
7
8
  import remove from "./route.interactionLink.remove"
8
9
  import restore from "./route.interactionLink.restore"
9
10
 
@@ -13,6 +14,7 @@ const registerRoutes = async (fastify: PluginFastifyInstance) => {
13
14
  await fastify.patch("/interactionLink/:_id", patch(fastify))
14
15
  await fastify.patch("/interactionLink/:_id/restore", restore(fastify))
15
16
  await fastify.post("/interactionLink", post(fastify))
17
+ await fastify.post("/interactionLink/bulk", postBulk(fastify))
16
18
  await fastify.delete("/interactionLink/:_id", remove(fastify))
17
19
  await fastify.delete("/interactionLink/:_id/hard", hardDelete(fastify))
18
20
  }
@@ -0,0 +1,60 @@
1
+ import type { InteractionLinkBody } from "../interactionLink.service"
2
+ import type { FastifyReply, FastifyRequest } from "fastify"
3
+ import type { PluginFastifyInstance } from "types"
4
+ import { interactionLinkCreateSchema } from "../interactionLink.schema"
5
+
6
+ export default (fastify: PluginFastifyInstance) => {
7
+ const interactionLinkService = fastify.custom.interactionLinkService
8
+
9
+ const handler = async (
10
+ req: FastifyRequest<{
11
+ Body: InteractionLinkBody[]
12
+ }>,
13
+ reply: FastifyReply,
14
+ ) => {
15
+ try {
16
+ // Validate that body is an array
17
+ if (!Array.isArray(req.body)) {
18
+ return reply.badRequest(
19
+ "Request body must be an array of interactionLinks",
20
+ )
21
+ }
22
+
23
+ // Validate that array is not empty
24
+ if (req.body.length === 0) {
25
+ return reply.badRequest("Request body cannot be an empty array")
26
+ }
27
+
28
+ const payload = await interactionLinkService.createInteractionLinks(
29
+ fastify,
30
+ req.requestParams,
31
+ req.body,
32
+ req.user._id,
33
+ )
34
+
35
+ return payload
36
+ } catch (error: any) {
37
+ fastify.log.error(error.message)
38
+
39
+ return reply.internalServerError(error.message)
40
+ }
41
+ }
42
+
43
+ return {
44
+ handler,
45
+ onRequest: [fastify.authenticate],
46
+ config: {
47
+ type: "dataCreate",
48
+ broadcastChannels: ["interactionLinksCreated"],
49
+ },
50
+ schema: {
51
+ summary: "Create multiple interactionLinks in bulk",
52
+ tags: ["core/interactionLink"],
53
+ body: {
54
+ type: "array",
55
+ items: interactionLinkCreateSchema,
56
+ minItems: 1,
57
+ },
58
+ },
59
+ }
60
+ }
@@ -4,6 +4,7 @@ import getAll from "./route.tag.getAll"
4
4
  import hardDelete from "./route.tag.hardDelete"
5
5
  import patch from "./route.tag.patch"
6
6
  import post from "./route.tag.post"
7
+ import postBulk from "./route.tag.post.bulk"
7
8
  import remove from "./route.tag.remove"
8
9
  import restore from "./route.tag.restore"
9
10
 
@@ -13,6 +14,7 @@ const registerRoutes = async (fastify: PluginFastifyInstance) => {
13
14
  await fastify.patch("/tag/:_id", patch(fastify))
14
15
  await fastify.patch("/tag/:_id/restore", restore(fastify))
15
16
  await fastify.post("/tag/:project/:_id", post(fastify))
17
+ await fastify.post("/tag/bulk", postBulk(fastify))
16
18
  await fastify.delete("/tag/:_id", remove(fastify))
17
19
  await fastify.delete("/tag/:_id/hard", hardDelete(fastify))
18
20
  }
@@ -0,0 +1,58 @@
1
+ import type { TagBody } from "../tag.service"
2
+ import type { FastifyReply, FastifyRequest } from "fastify"
3
+ import type { PluginFastifyInstance } from "types"
4
+ import { tagCreateSchema } from "../tag.schema"
5
+
6
+ export default (fastify: PluginFastifyInstance) => {
7
+ const tagService = fastify.custom.tagService
8
+
9
+ const handler = async (
10
+ req: FastifyRequest<{
11
+ Body: TagBody[]
12
+ }>,
13
+ reply: FastifyReply,
14
+ ) => {
15
+ try {
16
+ // Validate that body is an array
17
+ if (!Array.isArray(req.body)) {
18
+ return reply.badRequest("Request body must be an array of tags")
19
+ }
20
+
21
+ // Validate that array is not empty
22
+ if (req.body.length === 0) {
23
+ return reply.badRequest("Request body cannot be an empty array")
24
+ }
25
+
26
+ const payload = await tagService.createTags(
27
+ fastify,
28
+ req.requestParams,
29
+ req.body,
30
+ req.user._id,
31
+ )
32
+
33
+ return payload
34
+ } catch (error: any) {
35
+ fastify.log.error(error.message)
36
+
37
+ return reply.internalServerError(error.message)
38
+ }
39
+ }
40
+
41
+ return {
42
+ handler,
43
+ onRequest: [fastify.authenticate],
44
+ config: {
45
+ type: "dataCreate",
46
+ broadcastChannels: ["tagsCreated"],
47
+ },
48
+ schema: {
49
+ summary: "Create multiple tags in bulk",
50
+ tags: ["core/tag"],
51
+ body: {
52
+ type: "array",
53
+ items: tagCreateSchema,
54
+ minItems: 1,
55
+ },
56
+ },
57
+ }
58
+ }
@@ -8,7 +8,9 @@ import type { PluginFastifyInstance } from "types"
8
8
  import { v4 as uuidv4, validate } from "uuid"
9
9
  import { createTagPayload } from "./helpers/payload"
10
10
  import type { Model } from "mongoose"
11
-
11
+ export type TagBody = Partial<Omit<TagType, "_id">> & {
12
+ _id?: string
13
+ }
12
14
  export class TagService {
13
15
  private tagModel: Model<TagType>
14
16
 
@@ -369,6 +371,75 @@ export class TagService {
369
371
 
370
372
  fastify.emit("tagHardDeleted", payload)
371
373
 
374
+ return payload
375
+ }
376
+ /**
377
+ * Core function to create multiple tags (returns raw data)
378
+ */
379
+ async _createTags(
380
+ fastify: PluginFastifyInstance,
381
+ tagBodies: TagBody[],
382
+ userId: string,
383
+ ): Promise<TagType[]> {
384
+ try {
385
+ const tagsToCreate: TagBody[] = []
386
+
387
+ // Validate and prepare all tags
388
+ for (const tagBody of tagBodies) {
389
+ if (tagBody._id) {
390
+ const uuidValid = validate(tagBody._id)
391
+
392
+ if (!uuidValid) {
393
+ throw new Error(`Invalid ID - not a valid uuid v4: ${tagBody._id}`)
394
+ }
395
+
396
+ const duplicate = await this.tagModel.findOne({
397
+ _id: tagBody._id,
398
+ })
399
+
400
+ if (duplicate) {
401
+ throw new Error(
402
+ `An entry with this id already exists: ${tagBody._id}`,
403
+ )
404
+ }
405
+ } else {
406
+ tagBody._id = uuidv4()
407
+ }
408
+
409
+ tagsToCreate.push({
410
+ ...tagBody,
411
+ author: userId,
412
+ lastEditor: userId,
413
+ })
414
+ }
415
+
416
+ // Bulk insert
417
+ const tags = await this.tagModel.insertMany(tagsToCreate)
418
+
419
+ fastify.log.info(`[API] Created ${tags.length} tags in bulk`)
420
+
421
+ return tags.map((comp) => (comp.toObject ? comp.toObject() : comp))
422
+ } catch (error: any) {
423
+ fastify.log.error(error.message)
424
+ throw error
425
+ }
426
+ }
427
+
428
+ /**
429
+ * Create multiple tags with payload wrapping and event emission
430
+ */
431
+ async createTags(
432
+ fastify: PluginFastifyInstance,
433
+ requestData: FrontendPayloadRequestData,
434
+ tagBodies: TagBody[],
435
+ userId: string,
436
+ ): Promise<FrontendPayload<TagType[]>> {
437
+ const tags = await this._createTags(fastify, tagBodies, userId)
438
+
439
+ const payload = await createTagPayload(fastify, tags, requestData)
440
+
441
+ fastify.emit("tagsCreated", payload)
442
+
372
443
  return payload
373
444
  }
374
445
  }
@@ -29,7 +29,16 @@ const $writeDataApi = (
29
29
  const _executeParams = clone(executeParams)
30
30
  errorSubject$.next(null)
31
31
  responseSubject$.next({})
32
- const queryParams = mergeDeepRight(_params, _executeParams)
32
+ let queryParams
33
+ if (Array.isArray(_executeParams)) {
34
+ if (Array.isArray(_params)) {
35
+ queryParams = _params.concat(_executeParams)
36
+ }
37
+ queryParams = _executeParams
38
+ } else {
39
+ queryParams = mergeDeepRight(_params, _executeParams)
40
+ }
41
+
33
42
  if (options.responseType === "stream") {
34
43
  return writeData(actionId, operationDefinition, queryParams, options)
35
44
  }
@@ -7,11 +7,13 @@ export default () => {
7
7
 
8
8
  // This function allows you to turn on and off the snow
9
9
  const toggle_snow = () => {
10
- const check_box = document.getElementById("toggle_snow")
10
+ const check_box = document.getElementById(
11
+ "toggle_snow",
12
+ ) as HTMLInputElement | null
11
13
  const snowEl = document.getElementById("snow")
12
14
 
13
15
  if (snowEl) {
14
- snowEl.style.display = check_box.checked ? "block" : "none"
16
+ snowEl.style.display = check_box?.checked ? "block" : "none"
15
17
  }
16
18
  }
17
19
 
@@ -29,87 +31,74 @@ export default () => {
29
31
  const randomDuration = 8 + Math.random() * 15 // Duration between 8-23s
30
32
  const randomDelay = Math.random() * 10 // Delay between 0-10s
31
33
  const randomOpacity = 0.3 + Math.random() * 0.7 // Opacity between 0.3-1
32
- const randomAnimation = Math.floor(Math.random() * 5) + 1 // Choose from 5 animations
34
+ const randomAnimation = Math.floor(Math.random() * 3) + 1 // Choose from 3 animations
33
35
 
34
- snowflake.style.setProperty("--start-x", `${randomX}vw`)
35
- snowflake.style.setProperty("--scale", randomScale)
36
+ snowflake.style.setProperty("--start-x", `${randomX}%`)
37
+ snowflake.style.setProperty("--scale", randomScale.toString())
36
38
  snowflake.style.setProperty("--duration", `${randomDuration}s`)
37
39
  snowflake.style.setProperty("--delay", `${randomDelay}s`)
38
- snowflake.style.setProperty("--opacity", randomOpacity)
40
+ snowflake.style.setProperty("--opacity", randomOpacity.toString())
39
41
  snowflake.style.setProperty("--animation-name", `fall-${randomAnimation}`)
40
42
 
41
43
  fragment.appendChild(snowflake)
42
44
  }
43
45
 
44
- document.getElementById("snow").appendChild(fragment)
46
+ document.getElementById("snow")?.appendChild(fragment)
45
47
  }
46
48
 
47
- // Optimized CSS with reusable animations and CSS custom properties
49
+ // Optimized CSS with GPU acceleration and simplified animations
48
50
  const createOptimizedCSS = () => {
49
51
  const css = `
50
52
  #snow {
51
53
  position: fixed;
52
54
  top: 0;
53
55
  left: 0;
54
- width: 100%;
55
- height: 100%;
56
+ right: 0;
57
+ bottom: 0;
56
58
  pointer-events: none;
57
59
  z-index: 1000;
60
+ overflow: hidden;
58
61
  }
59
62
 
60
63
  .snowflake {
61
64
  position: absolute;
65
+ top: -10px;
62
66
  width: 8px;
63
67
  height: 8px;
64
68
  background: white;
65
69
  border-radius: 50%;
66
- filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.8));
70
+
71
+ /* GPU acceleration - critical for performance */
67
72
  will-change: transform;
73
+ transform: translateZ(0);
74
+ backface-visibility: hidden;
75
+
76
+ /* Simplified glow effect - much lighter than drop-shadow filter */
77
+ box-shadow: 0 0 6px rgba(255, 255, 255, 0.6);
68
78
 
69
79
  /* Use CSS custom properties for individual variations */
70
80
  left: var(--start-x);
71
81
  opacity: var(--opacity);
72
- transform: scale(var(--scale));
73
82
  animation: var(--animation-name) var(--duration) var(--delay) linear infinite;
74
83
  }
75
84
 
76
- /* 5 different falling patterns for variety */
85
+ /* Simplified to 3 falling patterns - uses only transform for best performance */
77
86
  @keyframes fall-1 {
78
- 0% { transform: translateY(-10px) translateX(0) scale(var(--scale)); }
79
- 25% { transform: translateY(25vh) translateX(10px) scale(var(--scale)); }
80
- 50% { transform: translateY(50vh) translateX(-5px) scale(var(--scale)); }
81
- 75% { transform: translateY(75vh) translateX(8px) scale(var(--scale)); }
82
- 100% { transform: translateY(100vh) translateX(0) scale(var(--scale)); }
87
+ to {
88
+ transform: translateY(100vh) translateX(20px) scale(var(--scale)) translateZ(0);
89
+ }
83
90
  }
84
91
 
85
92
  @keyframes fall-2 {
86
- 0% { transform: translateY(-10px) translateX(0) scale(var(--scale)); }
87
- 30% { transform: translateY(30vh) translateX(-15px) scale(var(--scale)); }
88
- 60% { transform: translateY(60vh) translateX(10px) scale(var(--scale)); }
89
- 100% { transform: translateY(100vh) translateX(-5px) scale(var(--scale)); }
93
+ to {
94
+ transform: translateY(100vh) translateX(-20px) scale(var(--scale)) translateZ(0);
95
+ }
90
96
  }
91
97
 
92
98
  @keyframes fall-3 {
93
- 0% { transform: translateY(-10px) translateX(0) scale(var(--scale)); }
94
- 20% { transform: translateY(20vh) translateX(5px) scale(var(--scale)); }
95
- 40% { transform: translateY(40vh) translateX(-10px) scale(var(--scale)); }
96
- 70% { transform: translateY(70vh) translateX(15px) scale(var(--scale)); }
97
- 100% { transform: translateY(100vh) translateX(0) scale(var(--scale)); }
98
- }
99
-
100
- @keyframes fall-4 {
101
- 0% { transform: translateY(-10px) translateX(0) scale(var(--scale)); }
102
- 35% { transform: translateY(35vh) translateX(20px) scale(var(--scale)); }
103
- 65% { transform: translateY(65vh) translateX(-8px) scale(var(--scale)); }
104
- 100% { transform: translateY(100vh) translateX(12px) scale(var(--scale)); }
105
- }
106
-
107
- @keyframes fall-5 {
108
- 0% { transform: translateY(-10px) translateX(0) scale(var(--scale)); }
109
- 15% { transform: translateY(15vh) translateX(-5px) scale(var(--scale)); }
110
- 45% { transform: translateY(45vh) translateX(12px) scale(var(--scale)); }
111
- 80% { transform: translateY(80vh) translateX(-7px) scale(var(--scale)); }
112
- 100% { transform: translateY(100vh) translateX(3px) scale(var(--scale)); }
99
+ to {
100
+ transform: translateY(100vh) translateX(0px) scale(var(--scale)) translateZ(0);
101
+ }
113
102
  }
114
103
  `
115
104
 
@@ -142,7 +131,7 @@ export default () => {
142
131
  } else {
143
132
  // Remove existing snow
144
133
  snowEl.remove()
145
- // Optionally remove styles as well
134
+ // Remove styles as well
146
135
  const styles = document.getElementById("snow-styles")
147
136
  if (styles) {
148
137
  styles.remove()
@@ -75,8 +75,22 @@
75
75
  "openInFloatingWindow": "In extrafenster öffnen",
76
76
  "showDeleted": "Zeige gelöschte",
77
77
  "hideDeleted": "Verstecke gelöschte",
78
+ "selectAll": "Alle auswählen",
79
+ "selected": "ausgewählt",
80
+ "noItemsSelected": "Keine Elemente ausgewählt. Bitte wählen Sie mindestens ein Element aus, um fortzufahren.",
78
81
  "cancel": "Abbrechen",
79
82
  "close": "Schließen",
83
+ "itemsCount": "Anzahl der Elemente",
84
+ "selectedItems": "Ausgewählte Elemente",
85
+ "selectItemsToExport": "Elemente zum Exportieren auswählen",
86
+ "export": "Exportieren",
87
+ "selectItemsToImport": "Elemente zum Importieren auswählen",
88
+ "import": "Importieren",
89
+ "exportSummary": "Export-Zusammenfassung",
90
+ "exportSuccessMessage": "Elemente wurden erfolgreich exportiert",
91
+ "importSummary": "Import-Zusammenfassung",
92
+ "importSuccessMessage": "Elemente bereit zum Importieren",
93
+ "saveImported": "Importierte Elemente speichern",
80
94
  "clear": "Leeren",
81
95
  "copyToClipboard$name": "{name} in Zwischenablage kopieren",
82
96
  "timeAgo": "vor {time}",
@@ -75,8 +75,22 @@
75
75
  "openInFloatingWindow": "Open in floating window",
76
76
  "showDeleted": "Show deleted",
77
77
  "hideDeleted": "Hide deleted",
78
+ "selectAll": "Select All",
79
+ "selected": "selected",
80
+ "noItemsSelected": "No items selected. Please select at least one item to continue.",
78
81
  "cancel": "Cancel",
79
82
  "close": "Close",
83
+ "itemsCount": "Items count",
84
+ "selectedItems": "Selected items",
85
+ "selectItemsToExport": "Select Items to Export",
86
+ "export": "Export",
87
+ "selectItemsToImport": "Select Items to Import",
88
+ "import": "Import",
89
+ "exportSummary": "Export Summary",
90
+ "exportSuccessMessage": "Items have been successfully exported",
91
+ "importSummary": "Import Summary",
92
+ "importSuccessMessage": "Items ready to import",
93
+ "saveImported": "Save Imported Items",
80
94
  "clear": "Clear",
81
95
  "copyToClipboard$name": "Copy {name} to clipboard",
82
96
  "timeAgo": "{time} ago",
@@ -75,8 +75,22 @@
75
75
  "openInFloatingWindow": "Otvoriť v plávajúcom okne",
76
76
  "showDeleted": "relácia vymazaná",
77
77
  "hideDeleted": "skryť odstránené",
78
+ "selectAll": "Vybrať všetko",
79
+ "selected": "vybrané",
80
+ "noItemsSelected": "Nie sú vybrané žiadne položky. Vyberte aspoň jednu položku pre pokračovanie.",
78
81
  "cancel": "Zrušiť",
79
82
  "close": "Zavrieť",
83
+ "itemsCount": "Počet položiek",
84
+ "selectedItems": "Vybrané položky",
85
+ "selectItemsToExport": "Vyberte položky na export",
86
+ "export": "Exportovať",
87
+ "selectItemsToImport": "Vyberte položky na import",
88
+ "import": "Importovať",
89
+ "exportSummary": "Súhrn exportu",
90
+ "exportSuccessMessage": "Položky boli úspešne exportované",
91
+ "importSummary": "Súhrn importu",
92
+ "importSuccessMessage": "Položky pripravené na import",
93
+ "saveImported": "Uložiť importované položky",
80
94
  "clear": "Vyčistiť",
81
95
  "copyToClipboard$name": "Skopírovať {name} do schránky",
82
96
  "timeAgo": "pred {time}",
package/yarn.lock CHANGED
@@ -295,10 +295,10 @@
295
295
  resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.9.tgz#d229a7b7f9dac167a156992ef23c7f023653f53b"
296
296
  integrity sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==
297
297
 
298
- "@raclettejs/types@^0.1.18":
299
- version "0.1.18"
300
- resolved "https://registry.yarnpkg.com/@raclettejs/types/-/types-0.1.18.tgz#7df0eb1b86db015694fabfafa089a277e654b350"
301
- integrity sha512-mvQI2sO48z6+QKj/s5A5JMBKnY8piE+8D8N0soctfsU7tAKg8nEwlxOiQHZiqJyR1k+gX2p+VBMRGbxHCYAGFw==
298
+ "@raclettejs/types@^0.1.19":
299
+ version "0.1.19"
300
+ resolved "https://registry.yarnpkg.com/@raclettejs/types/-/types-0.1.19.tgz#5f17afcc3b8f2d4849c828648711c426fdb2f74d"
301
+ integrity sha512-EdIMglaM+ivsvkVaXZKVKBoyXsLivulloveYPEV70Lm1iLeja6l7IywyM82dMdEgNMmpEMlG8r2TjHyXu9MPkg==
302
302
  dependencies:
303
303
  "@types/node" "24.5.2"
304
304
  fastify "5.6.0"