@js-gamifications/word-search-angular 1.0.1

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/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # @js-gamifications/word-search-angular
2
+
3
+ Angular adapter for the word search core package.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm i @js-gamifications/word-search-angular
9
+ ```
10
+
11
+ `@js-gamifications/word-search-core` is installed automatically as a dependency.
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { Component } from "@angular/core";
17
+ import { WordSearchComponent } from "@js-gamifications/word-search-angular";
18
+
19
+ @Component({
20
+ selector: "app-root",
21
+ standalone: true,
22
+ imports: [WordSearchComponent],
23
+ template: `
24
+ <js-gamifications-word-search
25
+ [rows]="10"
26
+ [cols]="10"
27
+ [words]="['angular', 'signals', 'components']"
28
+ [allowDiagonal]="true"
29
+ />
30
+ `
31
+ })
32
+ export class AppComponent {}
33
+ ```
package/dist/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # @js-gamifications/word-search-angular
2
+
3
+ Angular adapter for the word search core package.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm i @js-gamifications/word-search-angular
9
+ ```
10
+
11
+ `@js-gamifications/word-search-core` is installed automatically as a dependency.
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { Component } from "@angular/core";
17
+ import { WordSearchComponent } from "@js-gamifications/word-search-angular";
18
+
19
+ @Component({
20
+ selector: "app-root",
21
+ standalone: true,
22
+ imports: [WordSearchComponent],
23
+ template: `
24
+ <js-gamifications-word-search
25
+ [rows]="10"
26
+ [cols]="10"
27
+ [words]="['angular', 'signals', 'components']"
28
+ [allowDiagonal]="true"
29
+ />
30
+ `
31
+ })
32
+ export class AppComponent {}
33
+ ```
@@ -0,0 +1,555 @@
1
+ import * as i0 from '@angular/core';
2
+ import { Injectable, EventEmitter, HostListener, Output, Input, Component } from '@angular/core';
3
+ import { createWordSearch } from '@js-gamifications/word-search-core';
4
+ import * as i1 from '@angular/common';
5
+ import { CommonModule } from '@angular/common';
6
+ import * as i2 from '@angular/forms';
7
+ import { FormsModule } from '@angular/forms';
8
+
9
+ class WordSearchService {
10
+ generate(options) {
11
+ return createWordSearch(options);
12
+ }
13
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: WordSearchService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
14
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: WordSearchService, providedIn: "root" });
15
+ }
16
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: WordSearchService, decorators: [{
17
+ type: Injectable,
18
+ args: [{ providedIn: "root" }]
19
+ }] });
20
+
21
+ const TRANSLATIONS = {
22
+ ES: {
23
+ title: "Busqueda de Palabras",
24
+ found: "encontradas",
25
+ instructions: "Haz clic y arrastra sobre las letras para seleccionar una palabra.",
26
+ clearProgress: "Reiniciar",
27
+ newPuzzle: "Nuevo Juego",
28
+ words: "Palabras",
29
+ allWordsFound: "Todas las palabras encontradas",
30
+ timer: "Tiempo",
31
+ completionTitle: "Puzzle completado",
32
+ completionMessage: "Excelente trabajo, encontraste todas las palabras.",
33
+ completionTime: "Tiempo total",
34
+ completionWords: "Palabras encontradas",
35
+ completionGrid: "Tamano de tablero",
36
+ completionDiagonal: "Diagonal habilitada",
37
+ completionAt: "Completado a las",
38
+ yes: "Si",
39
+ no: "No"
40
+ },
41
+ EN: {
42
+ title: "Word Search",
43
+ found: "found",
44
+ instructions: "Hold click and drag over letters to select a word.",
45
+ clearProgress: "Clear Progress",
46
+ newPuzzle: "New Puzzle",
47
+ words: "Words",
48
+ allWordsFound: "All words found",
49
+ timer: "Time",
50
+ completionTitle: "Puzzle Completed",
51
+ completionMessage: "Great job, you found every word.",
52
+ completionTime: "Total time",
53
+ completionWords: "Words found",
54
+ completionGrid: "Grid size",
55
+ completionDiagonal: "Diagonal enabled",
56
+ completionAt: "Completed at",
57
+ yes: "Yes",
58
+ no: "No"
59
+ },
60
+ AR: {
61
+ title: "البحث عن الكلمات",
62
+ found: "موجودة",
63
+ instructions: "اضغط واسحب فوق الحروف لتحديد كلمة",
64
+ clearProgress: "مسح التقدم",
65
+ newPuzzle: "لعبة جديدة",
66
+ words: "الكلمات",
67
+ allWordsFound: "تم العثور على جميع الكلمات",
68
+ timer: "الوقت",
69
+ completionTitle: "اكتمل اللغز",
70
+ completionMessage: "عمل رائع، لقد عثرت على جميع الكلمات",
71
+ completionTime: "الوقت الكلي",
72
+ completionWords: "الكلمات التي تم العثور عليها",
73
+ completionGrid: "حجم الشبكة",
74
+ completionDiagonal: "القطري مفعل",
75
+ completionAt: "اكتمل في",
76
+ yes: "نعم",
77
+ no: "لا"
78
+ }
79
+ };
80
+ function playSound(frequency, duration) {
81
+ if (typeof window === "undefined") {
82
+ return;
83
+ }
84
+ const Ctx = window.AudioContext || window.webkitAudioContext;
85
+ if (!Ctx) {
86
+ return;
87
+ }
88
+ const audioContext = new Ctx();
89
+ const oscillator = audioContext.createOscillator();
90
+ const gainNode = audioContext.createGain();
91
+ oscillator.connect(gainNode);
92
+ gainNode.connect(audioContext.destination);
93
+ oscillator.frequency.value = frequency;
94
+ oscillator.type = "sine";
95
+ gainNode.gain.setValueAtTime(0.25, audioContext.currentTime);
96
+ gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + duration);
97
+ oscillator.start(audioContext.currentTime);
98
+ oscillator.stop(audioContext.currentTime + duration);
99
+ }
100
+ function playSuccessSound() {
101
+ playSound(800, 0.15);
102
+ setTimeout(() => playSound(1000, 0.15), 150);
103
+ }
104
+ function playWinnerSound() {
105
+ playSound(523.25, 0.2);
106
+ setTimeout(() => playSound(659.25, 0.2), 220);
107
+ setTimeout(() => playSound(783.99, 0.4), 440);
108
+ }
109
+ function normalizeWord(word) {
110
+ return word.trim().toUpperCase().replace(/\s+/g, "");
111
+ }
112
+ function toCellKey(cell) {
113
+ return `${cell.row}:${cell.col}`;
114
+ }
115
+ function toPathFromPlacement(placement) {
116
+ return Array.from({ length: placement.word.length }, (_, index) => ({
117
+ row: placement.startRow + placement.deltaRow * index,
118
+ col: placement.startCol + placement.deltaCol * index
119
+ }));
120
+ }
121
+ function buildPath(start, end) {
122
+ const rowDiff = end.row - start.row;
123
+ const colDiff = end.col - start.col;
124
+ const absRowDiff = Math.abs(rowDiff);
125
+ const absColDiff = Math.abs(colDiff);
126
+ const isHorizontal = rowDiff === 0;
127
+ const isVertical = colDiff === 0;
128
+ const isDiagonal = absRowDiff === absColDiff;
129
+ if (!isHorizontal && !isVertical && !isDiagonal) {
130
+ return [start];
131
+ }
132
+ const rowStep = rowDiff === 0 ? 0 : rowDiff / absRowDiff;
133
+ const colStep = colDiff === 0 ? 0 : colDiff / absColDiff;
134
+ const steps = Math.max(absRowDiff, absColDiff);
135
+ return Array.from({ length: steps + 1 }, (_, index) => ({
136
+ row: start.row + rowStep * index,
137
+ col: start.col + colStep * index
138
+ }));
139
+ }
140
+ function matchesPlacement(path, placement) {
141
+ if (path.length !== placement.word.length) {
142
+ return false;
143
+ }
144
+ const forward = toPathFromPlacement(placement);
145
+ const matchesForward = forward.every((expected, index) => {
146
+ const selected = path[index];
147
+ return selected?.row === expected.row && selected?.col === expected.col;
148
+ });
149
+ if (matchesForward) {
150
+ return true;
151
+ }
152
+ return forward.every((expected, index) => {
153
+ const selected = path[path.length - 1 - index];
154
+ return selected?.row === expected.row && selected?.col === expected.col;
155
+ });
156
+ }
157
+ class WordSearchComponent {
158
+ rows = 10;
159
+ cols = 10;
160
+ words = [];
161
+ allowDiagonal = true;
162
+ completed = new EventEmitter();
163
+ generated = new EventEmitter();
164
+ puzzle = null;
165
+ language = "EN";
166
+ elapsedSeconds = 0;
167
+ showCelebration = false;
168
+ completionReport = null;
169
+ foundWords = new Set();
170
+ normalizedInputWords = [];
171
+ pendingWords = [];
172
+ foundCellKeys = new Set();
173
+ currentPathKeys = new Set();
174
+ isDragging = false;
175
+ dragStart = null;
176
+ dragCurrent = null;
177
+ puzzleSeed = 0;
178
+ boardSize = 280;
179
+ timerHandle = null;
180
+ celebrationTimeout = null;
181
+ get t() {
182
+ return TRANSLATIONS[this.language];
183
+ }
184
+ ngOnInit() {
185
+ this.timerHandle = setInterval(() => {
186
+ this.elapsedSeconds += 1;
187
+ }, 1000);
188
+ this.generatePuzzle();
189
+ }
190
+ ngOnChanges() {
191
+ this.normalizedInputWords = this.words.map(normalizeWord).filter(Boolean);
192
+ this.pendingWords = [...this.normalizedInputWords];
193
+ this.boardSize = Math.min(560, Math.max(280, Math.max(this.rows, this.cols) * 38));
194
+ if (!this.words.length) {
195
+ this.puzzle = null;
196
+ this.clearRoundState();
197
+ return;
198
+ }
199
+ if (this.puzzle) {
200
+ this.generatePuzzle();
201
+ }
202
+ }
203
+ ngOnDestroy() {
204
+ if (this.timerHandle) {
205
+ clearInterval(this.timerHandle);
206
+ }
207
+ if (this.celebrationTimeout) {
208
+ clearTimeout(this.celebrationTimeout);
209
+ }
210
+ }
211
+ onDocumentMouseUp() {
212
+ this.finishSelection();
213
+ }
214
+ setLanguage(value) {
215
+ this.language = value;
216
+ }
217
+ formatTime(seconds) {
218
+ const hours = Math.floor(seconds / 3600);
219
+ const minutes = Math.floor((seconds % 3600) / 60);
220
+ const secs = seconds % 60;
221
+ if (hours > 0) {
222
+ return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
223
+ }
224
+ return `${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
225
+ }
226
+ formatCompletedAt(timestamp) {
227
+ return new Date(timestamp).toLocaleTimeString();
228
+ }
229
+ isFoundCell(row, col) {
230
+ return this.foundCellKeys.has(`${row}:${col}`);
231
+ }
232
+ isPathCell(row, col) {
233
+ return this.currentPathKeys.has(`${row}:${col}`);
234
+ }
235
+ isWordFound(word) {
236
+ return this.foundWords.has(word);
237
+ }
238
+ startSelection(row, col) {
239
+ this.isDragging = true;
240
+ this.dragStart = { row, col };
241
+ this.dragCurrent = { row, col };
242
+ this.refreshCurrentPathKeys();
243
+ }
244
+ moveSelection(row, col) {
245
+ if (!this.isDragging) {
246
+ return;
247
+ }
248
+ this.dragCurrent = { row, col };
249
+ this.refreshCurrentPathKeys();
250
+ }
251
+ finishSelection() {
252
+ if (!this.isDragging || !this.dragStart || !this.dragCurrent || !this.puzzle) {
253
+ this.clearDragState();
254
+ return;
255
+ }
256
+ const path = buildPath(this.dragStart, this.dragCurrent);
257
+ const match = this.puzzle.placements.find((placement) => matchesPlacement(path, placement));
258
+ if (match && !this.foundWords.has(match.word)) {
259
+ this.foundWords.add(match.word);
260
+ this.refreshFoundCellKeys();
261
+ this.pendingWords = this.normalizedInputWords.filter((word) => !this.foundWords.has(word));
262
+ playSuccessSound();
263
+ if (this.foundWords.size === this.normalizedInputWords.length) {
264
+ const report = {
265
+ elapsedSeconds: this.elapsedSeconds,
266
+ foundWords: this.foundWords.size,
267
+ totalWords: this.normalizedInputWords.length,
268
+ rows: this.rows,
269
+ cols: this.cols,
270
+ allowDiagonal: this.allowDiagonal,
271
+ completedAt: new Date().toISOString()
272
+ };
273
+ this.completionReport = report;
274
+ this.completed.emit(report);
275
+ playWinnerSound();
276
+ this.showCelebration = true;
277
+ if (this.celebrationTimeout) {
278
+ clearTimeout(this.celebrationTimeout);
279
+ }
280
+ this.celebrationTimeout = setTimeout(() => {
281
+ this.showCelebration = false;
282
+ }, 3000);
283
+ }
284
+ }
285
+ this.clearDragState();
286
+ }
287
+ resetProgress() {
288
+ this.foundWords = new Set();
289
+ this.pendingWords = [...this.normalizedInputWords];
290
+ this.foundCellKeys = new Set();
291
+ this.completionReport = null;
292
+ this.showCelebration = false;
293
+ this.clearDragState();
294
+ }
295
+ generateNewPuzzle() {
296
+ this.puzzleSeed += 1;
297
+ this.generatePuzzle();
298
+ }
299
+ generatePuzzle() {
300
+ if (this.words.length === 0) {
301
+ this.puzzle = null;
302
+ this.clearRoundState();
303
+ return;
304
+ }
305
+ this.puzzle = createWordSearch({
306
+ rows: this.rows,
307
+ cols: this.cols,
308
+ words: this.words,
309
+ allowDiagonal: this.allowDiagonal
310
+ });
311
+ this.generated.emit(this.puzzle);
312
+ this.elapsedSeconds = 0;
313
+ this.resetProgress();
314
+ }
315
+ refreshFoundCellKeys() {
316
+ const next = new Set();
317
+ if (!this.puzzle) {
318
+ this.foundCellKeys = next;
319
+ return;
320
+ }
321
+ for (const placement of this.puzzle.placements) {
322
+ if (!this.foundWords.has(placement.word)) {
323
+ continue;
324
+ }
325
+ for (const cell of toPathFromPlacement(placement)) {
326
+ next.add(toCellKey(cell));
327
+ }
328
+ }
329
+ this.foundCellKeys = next;
330
+ }
331
+ refreshCurrentPathKeys() {
332
+ if (!this.dragStart || !this.dragCurrent) {
333
+ this.currentPathKeys = new Set();
334
+ return;
335
+ }
336
+ const path = buildPath(this.dragStart, this.dragCurrent);
337
+ const next = new Set();
338
+ for (const cell of path) {
339
+ next.add(toCellKey(cell));
340
+ }
341
+ this.currentPathKeys = next;
342
+ }
343
+ clearDragState() {
344
+ this.isDragging = false;
345
+ this.dragStart = null;
346
+ this.dragCurrent = null;
347
+ this.currentPathKeys = new Set();
348
+ }
349
+ clearRoundState() {
350
+ this.elapsedSeconds = 0;
351
+ this.showCelebration = false;
352
+ this.completionReport = null;
353
+ this.foundWords = new Set();
354
+ this.pendingWords = [...this.normalizedInputWords];
355
+ this.foundCellKeys = new Set();
356
+ this.clearDragState();
357
+ if (this.celebrationTimeout) {
358
+ clearTimeout(this.celebrationTimeout);
359
+ this.celebrationTimeout = null;
360
+ }
361
+ }
362
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: WordSearchComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
363
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", type: WordSearchComponent, isStandalone: true, selector: "js-gamifications-word-search", inputs: { rows: "rows", cols: "cols", words: "words", allowDiagonal: "allowDiagonal" }, outputs: { completed: "completed", generated: "generated" }, host: { listeners: { "document:mouseup": "onDocumentMouseUp()" } }, usesOnChanges: true, ngImport: i0, template: `
364
+ <div class="word-search-root" [class.rtl]="language === 'AR'">
365
+ <div class="celebration-overlay" *ngIf="showCelebration">
366
+ <div class="celebration-glow"></div>
367
+ <div class="celebration-emoji">🎉</div>
368
+ </div>
369
+
370
+ <div class="header-row">
371
+ <div class="title-wrap">
372
+ <strong class="title">{{ t.title }}</strong>
373
+ <span class="meta-chip">{{ foundWords.size }}/{{ normalizedInputWords.length }} {{ t.found }}</span>
374
+ <span class="meta-chip">{{ t.timer }}: {{ formatTime(elapsedSeconds) }}</span>
375
+ </div>
376
+ <div class="instructions">{{ t.instructions }}</div>
377
+ </div>
378
+
379
+ <div class="controls-row">
380
+ <div class="language-wrap">
381
+ <label>Language:</label>
382
+ <select [ngModel]="language" (ngModelChange)="setLanguage($event)">
383
+ <option value="EN">English</option>
384
+ <option value="ES">Español</option>
385
+ <option value="AR">العربية</option>
386
+ </select>
387
+ </div>
388
+
389
+ <div class="buttons-wrap">
390
+ <button type="button" class="btn btn-secondary" (click)="resetProgress()">{{ t.clearProgress }}</button>
391
+ <button type="button" class="btn btn-primary" (click)="generateNewPuzzle()">{{ t.newPuzzle }}</button>
392
+ </div>
393
+ </div>
394
+
395
+ <div class="board-wrapper">
396
+ <div
397
+ class="board-grid"
398
+ [style.maxWidth.px]="boardSize"
399
+ [style.gridTemplateColumns]="'repeat(' + cols + ', minmax(0, 1fr))'"
400
+ [style.gridTemplateRows]="'repeat(' + rows + ', minmax(0, 1fr))'"
401
+ (mouseleave)="finishSelection()"
402
+ >
403
+ <ng-container *ngIf="puzzle as currentPuzzle">
404
+ <ng-container *ngFor="let row of currentPuzzle.grid; let rowIndex = index">
405
+ <button
406
+ type="button"
407
+ class="word-search-cell"
408
+ *ngFor="let cell of row; let colIndex = index"
409
+ (mousedown)="startSelection(rowIndex, colIndex)"
410
+ (mouseenter)="moveSelection(rowIndex, colIndex)"
411
+ (mouseup)="finishSelection()"
412
+ [class.found-cell]="isFoundCell(rowIndex, colIndex)"
413
+ [class.path-cell]="isPathCell(rowIndex, colIndex)"
414
+ >
415
+ {{ cell }}
416
+ </button>
417
+ </ng-container>
418
+ </ng-container>
419
+ </div>
420
+ </div>
421
+
422
+ <div class="words-section">
423
+ <div class="words-title">{{ t.words }}</div>
424
+ <div class="words-list">
425
+ <span class="word-chip" [class.word-chip-found]="isWordFound(word)" *ngFor="let word of normalizedInputWords">
426
+ {{ word }}
427
+ </span>
428
+ </div>
429
+ <span class="all-found" *ngIf="pendingWords.length === 0">{{ t.allWordsFound }}</span>
430
+ </div>
431
+
432
+ <div class="completion-card" *ngIf="completionReport as report">
433
+ <div class="completion-title">{{ t.completionTitle }}</div>
434
+ <div class="completion-message">{{ t.completionMessage }}</div>
435
+ <div class="completion-grid">
436
+ <div><strong>{{ t.completionTime }}:</strong> {{ formatTime(report.elapsedSeconds) }}</div>
437
+ <div><strong>{{ t.completionWords }}:</strong> {{ report.foundWords }}/{{ report.totalWords }}</div>
438
+ <div><strong>{{ t.completionGrid }}:</strong> {{ report.rows }}x{{ report.cols }}</div>
439
+ <div><strong>{{ t.completionDiagonal }}:</strong> {{ report.allowDiagonal ? t.yes : t.no }}</div>
440
+ <div><strong>{{ t.completionAt }}:</strong> {{ formatCompletedAt(report.completedAt) }}</div>
441
+ </div>
442
+ </div>
443
+ </div>
444
+ `, isInline: true, styles: [".word-search-root{position:relative;width:100%;border-radius:16px;border:1px solid #e4e4e7;background:linear-gradient(180deg,#fff,#f8fafc);box-shadow:0 10px 30px #0f172a0f;padding:16px;overflow:hidden}.rtl{direction:rtl}.celebration-overlay{position:absolute;inset:0;pointer-events:none;z-index:20}.celebration-glow{position:absolute;inset:0;background:linear-gradient(45deg,#22c55e4d,#22c55e00);animation:pulse .6s ease-out}.celebration-emoji{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:48px;color:#22c55e;animation:bounce .6s ease-out}.header-row,.controls-row{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px;margin-bottom:12px}.title-wrap{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.title{font-size:16px;color:#111827}.meta-chip{font-size:12px;color:#374151;background:#f3f4f6;padding:4px 8px;border-radius:999px}.instructions,.language-wrap label,.words-title{font-size:12px;color:#6b7280}.language-wrap{display:flex;align-items:center;gap:6px}.language-wrap select{font-size:12px;padding:4px 8px;border-radius:6px;border:1px solid #d4d4d8;background:#fff}.buttons-wrap{display:flex;gap:8px}.btn{border-radius:8px;font-size:12px;padding:6px 10px;cursor:pointer;border:1px solid #d4d4d8}.btn-secondary{background:#fff;color:#374151}.btn-primary{background:#0ea5e9;color:#fff;border-color:#0ea5e9}.board-wrapper{width:100%;display:flex;justify-content:center}.board-grid{width:100%;aspect-ratio:1 / 1;display:grid;gap:1px;-webkit-user-select:none;user-select:none;touch-action:none}.word-search-cell{display:inline-flex;width:100%;height:100%;min-width:0;align-items:center;justify-content:center;border:1px solid #d4d4d8;border-radius:8px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-weight:700;font-size:clamp(12px,1.8vw,16px);color:#111827;background:#fff;transition:background .12s ease}.path-cell{background:#e0f2fe}.found-cell{background:#bae6fd}.words-section{margin-top:14px;display:grid;gap:8px}.words-list{display:flex;gap:8px;flex-wrap:wrap}.word-chip{display:inline-flex;align-items:center;padding:6px 10px;border-radius:999px;border:1px solid #d4d4d8;background:#fafafa;color:#3f3f46;font-size:12px;font-weight:600;letter-spacing:.04em}.word-chip-found{border-color:#22c55e;background:#dcfce7;color:#166534;text-decoration:line-through;text-decoration-thickness:2px}.all-found{font-size:12px;color:#16a34a;font-weight:600}.completion-card{margin-top:14px;border:1px solid #86efac;background:linear-gradient(180deg,#f0fdf4,#dcfce7);border-radius:12px;padding:12px}.completion-title{font-size:14px;font-weight:700;color:#166534;margin-bottom:4px}.completion-message{font-size:12px;color:#166534;margin-bottom:10px}.completion-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;font-size:12px;color:#14532d}@keyframes pulse{0%{opacity:1}to{opacity:0}}@keyframes bounce{0%,to{transform:translate(-50%,-50%) scale(1)}50%{transform:translate(-50%,-50%) scale(1.1)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
445
+ }
446
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: WordSearchComponent, decorators: [{
447
+ type: Component,
448
+ args: [{ selector: "js-gamifications-word-search", standalone: true, imports: [CommonModule, FormsModule], template: `
449
+ <div class="word-search-root" [class.rtl]="language === 'AR'">
450
+ <div class="celebration-overlay" *ngIf="showCelebration">
451
+ <div class="celebration-glow"></div>
452
+ <div class="celebration-emoji">🎉</div>
453
+ </div>
454
+
455
+ <div class="header-row">
456
+ <div class="title-wrap">
457
+ <strong class="title">{{ t.title }}</strong>
458
+ <span class="meta-chip">{{ foundWords.size }}/{{ normalizedInputWords.length }} {{ t.found }}</span>
459
+ <span class="meta-chip">{{ t.timer }}: {{ formatTime(elapsedSeconds) }}</span>
460
+ </div>
461
+ <div class="instructions">{{ t.instructions }}</div>
462
+ </div>
463
+
464
+ <div class="controls-row">
465
+ <div class="language-wrap">
466
+ <label>Language:</label>
467
+ <select [ngModel]="language" (ngModelChange)="setLanguage($event)">
468
+ <option value="EN">English</option>
469
+ <option value="ES">Español</option>
470
+ <option value="AR">العربية</option>
471
+ </select>
472
+ </div>
473
+
474
+ <div class="buttons-wrap">
475
+ <button type="button" class="btn btn-secondary" (click)="resetProgress()">{{ t.clearProgress }}</button>
476
+ <button type="button" class="btn btn-primary" (click)="generateNewPuzzle()">{{ t.newPuzzle }}</button>
477
+ </div>
478
+ </div>
479
+
480
+ <div class="board-wrapper">
481
+ <div
482
+ class="board-grid"
483
+ [style.maxWidth.px]="boardSize"
484
+ [style.gridTemplateColumns]="'repeat(' + cols + ', minmax(0, 1fr))'"
485
+ [style.gridTemplateRows]="'repeat(' + rows + ', minmax(0, 1fr))'"
486
+ (mouseleave)="finishSelection()"
487
+ >
488
+ <ng-container *ngIf="puzzle as currentPuzzle">
489
+ <ng-container *ngFor="let row of currentPuzzle.grid; let rowIndex = index">
490
+ <button
491
+ type="button"
492
+ class="word-search-cell"
493
+ *ngFor="let cell of row; let colIndex = index"
494
+ (mousedown)="startSelection(rowIndex, colIndex)"
495
+ (mouseenter)="moveSelection(rowIndex, colIndex)"
496
+ (mouseup)="finishSelection()"
497
+ [class.found-cell]="isFoundCell(rowIndex, colIndex)"
498
+ [class.path-cell]="isPathCell(rowIndex, colIndex)"
499
+ >
500
+ {{ cell }}
501
+ </button>
502
+ </ng-container>
503
+ </ng-container>
504
+ </div>
505
+ </div>
506
+
507
+ <div class="words-section">
508
+ <div class="words-title">{{ t.words }}</div>
509
+ <div class="words-list">
510
+ <span class="word-chip" [class.word-chip-found]="isWordFound(word)" *ngFor="let word of normalizedInputWords">
511
+ {{ word }}
512
+ </span>
513
+ </div>
514
+ <span class="all-found" *ngIf="pendingWords.length === 0">{{ t.allWordsFound }}</span>
515
+ </div>
516
+
517
+ <div class="completion-card" *ngIf="completionReport as report">
518
+ <div class="completion-title">{{ t.completionTitle }}</div>
519
+ <div class="completion-message">{{ t.completionMessage }}</div>
520
+ <div class="completion-grid">
521
+ <div><strong>{{ t.completionTime }}:</strong> {{ formatTime(report.elapsedSeconds) }}</div>
522
+ <div><strong>{{ t.completionWords }}:</strong> {{ report.foundWords }}/{{ report.totalWords }}</div>
523
+ <div><strong>{{ t.completionGrid }}:</strong> {{ report.rows }}x{{ report.cols }}</div>
524
+ <div><strong>{{ t.completionDiagonal }}:</strong> {{ report.allowDiagonal ? t.yes : t.no }}</div>
525
+ <div><strong>{{ t.completionAt }}:</strong> {{ formatCompletedAt(report.completedAt) }}</div>
526
+ </div>
527
+ </div>
528
+ </div>
529
+ `, styles: [".word-search-root{position:relative;width:100%;border-radius:16px;border:1px solid #e4e4e7;background:linear-gradient(180deg,#fff,#f8fafc);box-shadow:0 10px 30px #0f172a0f;padding:16px;overflow:hidden}.rtl{direction:rtl}.celebration-overlay{position:absolute;inset:0;pointer-events:none;z-index:20}.celebration-glow{position:absolute;inset:0;background:linear-gradient(45deg,#22c55e4d,#22c55e00);animation:pulse .6s ease-out}.celebration-emoji{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:48px;color:#22c55e;animation:bounce .6s ease-out}.header-row,.controls-row{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px;margin-bottom:12px}.title-wrap{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.title{font-size:16px;color:#111827}.meta-chip{font-size:12px;color:#374151;background:#f3f4f6;padding:4px 8px;border-radius:999px}.instructions,.language-wrap label,.words-title{font-size:12px;color:#6b7280}.language-wrap{display:flex;align-items:center;gap:6px}.language-wrap select{font-size:12px;padding:4px 8px;border-radius:6px;border:1px solid #d4d4d8;background:#fff}.buttons-wrap{display:flex;gap:8px}.btn{border-radius:8px;font-size:12px;padding:6px 10px;cursor:pointer;border:1px solid #d4d4d8}.btn-secondary{background:#fff;color:#374151}.btn-primary{background:#0ea5e9;color:#fff;border-color:#0ea5e9}.board-wrapper{width:100%;display:flex;justify-content:center}.board-grid{width:100%;aspect-ratio:1 / 1;display:grid;gap:1px;-webkit-user-select:none;user-select:none;touch-action:none}.word-search-cell{display:inline-flex;width:100%;height:100%;min-width:0;align-items:center;justify-content:center;border:1px solid #d4d4d8;border-radius:8px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-weight:700;font-size:clamp(12px,1.8vw,16px);color:#111827;background:#fff;transition:background .12s ease}.path-cell{background:#e0f2fe}.found-cell{background:#bae6fd}.words-section{margin-top:14px;display:grid;gap:8px}.words-list{display:flex;gap:8px;flex-wrap:wrap}.word-chip{display:inline-flex;align-items:center;padding:6px 10px;border-radius:999px;border:1px solid #d4d4d8;background:#fafafa;color:#3f3f46;font-size:12px;font-weight:600;letter-spacing:.04em}.word-chip-found{border-color:#22c55e;background:#dcfce7;color:#166534;text-decoration:line-through;text-decoration-thickness:2px}.all-found{font-size:12px;color:#16a34a;font-weight:600}.completion-card{margin-top:14px;border:1px solid #86efac;background:linear-gradient(180deg,#f0fdf4,#dcfce7);border-radius:12px;padding:12px}.completion-title{font-size:14px;font-weight:700;color:#166534;margin-bottom:4px}.completion-message{font-size:12px;color:#166534;margin-bottom:10px}.completion-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;font-size:12px;color:#14532d}@keyframes pulse{0%{opacity:1}to{opacity:0}}@keyframes bounce{0%,to{transform:translate(-50%,-50%) scale(1)}50%{transform:translate(-50%,-50%) scale(1.1)}}\n"] }]
530
+ }], propDecorators: { rows: [{
531
+ type: Input,
532
+ args: [{ required: true }]
533
+ }], cols: [{
534
+ type: Input,
535
+ args: [{ required: true }]
536
+ }], words: [{
537
+ type: Input,
538
+ args: [{ required: true }]
539
+ }], allowDiagonal: [{
540
+ type: Input
541
+ }], completed: [{
542
+ type: Output
543
+ }], generated: [{
544
+ type: Output
545
+ }], onDocumentMouseUp: [{
546
+ type: HostListener,
547
+ args: ["document:mouseup"]
548
+ }] } });
549
+
550
+ /**
551
+ * Generated bundle index. Do not edit.
552
+ */
553
+
554
+ export { WordSearchComponent, WordSearchService };
555
+ //# sourceMappingURL=js-gamifications-word-search-angular.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"js-gamifications-word-search-angular.mjs","sources":["../../src/word-search.service.ts","../../src/word-search.component.ts","../../src/js-gamifications-word-search-angular.ts"],"sourcesContent":["import { Injectable } from \"@angular/core\";\nimport { createWordSearch, type WordSearchOptions, type WordSearchPuzzle } from \"@js-gamifications/word-search-core\";\n\n@Injectable({ providedIn: \"root\" })\nexport class WordSearchService {\n generate(options: WordSearchOptions): WordSearchPuzzle {\n return createWordSearch(options);\n }\n}\n","import { CommonModule } from \"@angular/common\";\nimport { FormsModule } from \"@angular/forms\";\nimport { Component, EventEmitter, HostListener, Input, OnChanges, OnDestroy, OnInit, Output } from \"@angular/core\";\nimport { createWordSearch, type WordPlacement, type WordSearchPuzzle } from \"@js-gamifications/word-search-core\";\n\ntype LanguageCode = \"EN\" | \"ES\" | \"AR\";\n\nconst TRANSLATIONS = {\n ES: {\n title: \"Busqueda de Palabras\",\n found: \"encontradas\",\n instructions: \"Haz clic y arrastra sobre las letras para seleccionar una palabra.\",\n clearProgress: \"Reiniciar\",\n newPuzzle: \"Nuevo Juego\",\n words: \"Palabras\",\n allWordsFound: \"Todas las palabras encontradas\",\n timer: \"Tiempo\",\n completionTitle: \"Puzzle completado\",\n completionMessage: \"Excelente trabajo, encontraste todas las palabras.\",\n completionTime: \"Tiempo total\",\n completionWords: \"Palabras encontradas\",\n completionGrid: \"Tamano de tablero\",\n completionDiagonal: \"Diagonal habilitada\",\n completionAt: \"Completado a las\",\n yes: \"Si\",\n no: \"No\"\n },\n EN: {\n title: \"Word Search\",\n found: \"found\",\n instructions: \"Hold click and drag over letters to select a word.\",\n clearProgress: \"Clear Progress\",\n newPuzzle: \"New Puzzle\",\n words: \"Words\",\n allWordsFound: \"All words found\",\n timer: \"Time\",\n completionTitle: \"Puzzle Completed\",\n completionMessage: \"Great job, you found every word.\",\n completionTime: \"Total time\",\n completionWords: \"Words found\",\n completionGrid: \"Grid size\",\n completionDiagonal: \"Diagonal enabled\",\n completionAt: \"Completed at\",\n yes: \"Yes\",\n no: \"No\"\n },\n AR: {\n title: \"البحث عن الكلمات\",\n found: \"موجودة\",\n instructions: \"اضغط واسحب فوق الحروف لتحديد كلمة\",\n clearProgress: \"مسح التقدم\",\n newPuzzle: \"لعبة جديدة\",\n words: \"الكلمات\",\n allWordsFound: \"تم العثور على جميع الكلمات\",\n timer: \"الوقت\",\n completionTitle: \"اكتمل اللغز\",\n completionMessage: \"عمل رائع، لقد عثرت على جميع الكلمات\",\n completionTime: \"الوقت الكلي\",\n completionWords: \"الكلمات التي تم العثور عليها\",\n completionGrid: \"حجم الشبكة\",\n completionDiagonal: \"القطري مفعل\",\n completionAt: \"اكتمل في\",\n yes: \"نعم\",\n no: \"لا\"\n }\n} as const;\n\ninterface CellPosition {\n row: number;\n col: number;\n}\n\nexport interface WordSearchCompletionReport {\n elapsedSeconds: number;\n foundWords: number;\n totalWords: number;\n rows: number;\n cols: number;\n allowDiagonal: boolean;\n completedAt: string;\n}\n\nfunction playSound(frequency: number, duration: number) {\n if (typeof window === \"undefined\") {\n return;\n }\n\n const Ctx = window.AudioContext || (window as Window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n if (!Ctx) {\n return;\n }\n\n const audioContext = new Ctx();\n const oscillator = audioContext.createOscillator();\n const gainNode = audioContext.createGain();\n\n oscillator.connect(gainNode);\n gainNode.connect(audioContext.destination);\n oscillator.frequency.value = frequency;\n oscillator.type = \"sine\";\n\n gainNode.gain.setValueAtTime(0.25, audioContext.currentTime);\n gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + duration);\n\n oscillator.start(audioContext.currentTime);\n oscillator.stop(audioContext.currentTime + duration);\n}\n\nfunction playSuccessSound() {\n playSound(800, 0.15);\n setTimeout(() => playSound(1000, 0.15), 150);\n}\n\nfunction playWinnerSound() {\n playSound(523.25, 0.2);\n setTimeout(() => playSound(659.25, 0.2), 220);\n setTimeout(() => playSound(783.99, 0.4), 440);\n}\n\nfunction normalizeWord(word: string): string {\n return word.trim().toUpperCase().replace(/\\s+/g, \"\");\n}\n\nfunction toCellKey(cell: CellPosition): string {\n return `${cell.row}:${cell.col}`;\n}\n\nfunction toPathFromPlacement(placement: WordPlacement): CellPosition[] {\n return Array.from({ length: placement.word.length }, (_, index) => ({\n row: placement.startRow + placement.deltaRow * index,\n col: placement.startCol + placement.deltaCol * index\n }));\n}\n\nfunction buildPath(start: CellPosition, end: CellPosition): CellPosition[] {\n const rowDiff = end.row - start.row;\n const colDiff = end.col - start.col;\n const absRowDiff = Math.abs(rowDiff);\n const absColDiff = Math.abs(colDiff);\n\n const isHorizontal = rowDiff === 0;\n const isVertical = colDiff === 0;\n const isDiagonal = absRowDiff === absColDiff;\n\n if (!isHorizontal && !isVertical && !isDiagonal) {\n return [start];\n }\n\n const rowStep = rowDiff === 0 ? 0 : rowDiff / absRowDiff;\n const colStep = colDiff === 0 ? 0 : colDiff / absColDiff;\n const steps = Math.max(absRowDiff, absColDiff);\n\n return Array.from({ length: steps + 1 }, (_, index) => ({\n row: start.row + rowStep * index,\n col: start.col + colStep * index\n }));\n}\n\nfunction matchesPlacement(path: CellPosition[], placement: WordPlacement): boolean {\n if (path.length !== placement.word.length) {\n return false;\n }\n\n const forward = toPathFromPlacement(placement);\n const matchesForward = forward.every((expected, index) => {\n const selected = path[index];\n return selected?.row === expected.row && selected?.col === expected.col;\n });\n\n if (matchesForward) {\n return true;\n }\n\n return forward.every((expected, index) => {\n const selected = path[path.length - 1 - index];\n return selected?.row === expected.row && selected?.col === expected.col;\n });\n}\n\n@Component({\n selector: \"js-gamifications-word-search\",\n standalone: true,\n imports: [CommonModule, FormsModule],\n template: `\n <div class=\"word-search-root\" [class.rtl]=\"language === 'AR'\">\n <div class=\"celebration-overlay\" *ngIf=\"showCelebration\">\n <div class=\"celebration-glow\"></div>\n <div class=\"celebration-emoji\">🎉</div>\n </div>\n\n <div class=\"header-row\">\n <div class=\"title-wrap\">\n <strong class=\"title\">{{ t.title }}</strong>\n <span class=\"meta-chip\">{{ foundWords.size }}/{{ normalizedInputWords.length }} {{ t.found }}</span>\n <span class=\"meta-chip\">{{ t.timer }}: {{ formatTime(elapsedSeconds) }}</span>\n </div>\n <div class=\"instructions\">{{ t.instructions }}</div>\n </div>\n\n <div class=\"controls-row\">\n <div class=\"language-wrap\">\n <label>Language:</label>\n <select [ngModel]=\"language\" (ngModelChange)=\"setLanguage($event)\">\n <option value=\"EN\">English</option>\n <option value=\"ES\">Español</option>\n <option value=\"AR\">العربية</option>\n </select>\n </div>\n\n <div class=\"buttons-wrap\">\n <button type=\"button\" class=\"btn btn-secondary\" (click)=\"resetProgress()\">{{ t.clearProgress }}</button>\n <button type=\"button\" class=\"btn btn-primary\" (click)=\"generateNewPuzzle()\">{{ t.newPuzzle }}</button>\n </div>\n </div>\n\n <div class=\"board-wrapper\">\n <div\n class=\"board-grid\"\n [style.maxWidth.px]=\"boardSize\"\n [style.gridTemplateColumns]=\"'repeat(' + cols + ', minmax(0, 1fr))'\"\n [style.gridTemplateRows]=\"'repeat(' + rows + ', minmax(0, 1fr))'\"\n (mouseleave)=\"finishSelection()\"\n >\n <ng-container *ngIf=\"puzzle as currentPuzzle\">\n <ng-container *ngFor=\"let row of currentPuzzle.grid; let rowIndex = index\">\n <button\n type=\"button\"\n class=\"word-search-cell\"\n *ngFor=\"let cell of row; let colIndex = index\"\n (mousedown)=\"startSelection(rowIndex, colIndex)\"\n (mouseenter)=\"moveSelection(rowIndex, colIndex)\"\n (mouseup)=\"finishSelection()\"\n [class.found-cell]=\"isFoundCell(rowIndex, colIndex)\"\n [class.path-cell]=\"isPathCell(rowIndex, colIndex)\"\n >\n {{ cell }}\n </button>\n </ng-container>\n </ng-container>\n </div>\n </div>\n\n <div class=\"words-section\">\n <div class=\"words-title\">{{ t.words }}</div>\n <div class=\"words-list\">\n <span class=\"word-chip\" [class.word-chip-found]=\"isWordFound(word)\" *ngFor=\"let word of normalizedInputWords\">\n {{ word }}\n </span>\n </div>\n <span class=\"all-found\" *ngIf=\"pendingWords.length === 0\">{{ t.allWordsFound }}</span>\n </div>\n\n <div class=\"completion-card\" *ngIf=\"completionReport as report\">\n <div class=\"completion-title\">{{ t.completionTitle }}</div>\n <div class=\"completion-message\">{{ t.completionMessage }}</div>\n <div class=\"completion-grid\">\n <div><strong>{{ t.completionTime }}:</strong> {{ formatTime(report.elapsedSeconds) }}</div>\n <div><strong>{{ t.completionWords }}:</strong> {{ report.foundWords }}/{{ report.totalWords }}</div>\n <div><strong>{{ t.completionGrid }}:</strong> {{ report.rows }}x{{ report.cols }}</div>\n <div><strong>{{ t.completionDiagonal }}:</strong> {{ report.allowDiagonal ? t.yes : t.no }}</div>\n <div><strong>{{ t.completionAt }}:</strong> {{ formatCompletedAt(report.completedAt) }}</div>\n </div>\n </div>\n </div>\n `,\n styles: [\n `\n .word-search-root {\n position: relative;\n width: 100%;\n border-radius: 16px;\n border: 1px solid #e4e4e7;\n background: linear-gradient(180deg, #fff 0%, #f8fafc 100%);\n box-shadow: 0 10px 30px rgba(15, 23, 42, 0.06);\n padding: 16px;\n overflow: hidden;\n }\n\n .rtl {\n direction: rtl;\n }\n\n .celebration-overlay {\n position: absolute;\n inset: 0;\n pointer-events: none;\n z-index: 20;\n }\n\n .celebration-glow {\n position: absolute;\n inset: 0;\n background: linear-gradient(45deg, rgba(34, 197, 94, 0.3) 0%, rgba(34, 197, 94, 0) 100%);\n animation: pulse 0.6s ease-out;\n }\n\n .celebration-emoji {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n font-size: 48px;\n color: #22c55e;\n animation: bounce 0.6s ease-out;\n }\n\n .header-row,\n .controls-row {\n display: flex;\n justify-content: space-between;\n align-items: center;\n flex-wrap: wrap;\n gap: 10px;\n margin-bottom: 12px;\n }\n\n .title-wrap {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n }\n\n .title {\n font-size: 16px;\n color: #111827;\n }\n\n .meta-chip {\n font-size: 12px;\n color: #374151;\n background: #f3f4f6;\n padding: 4px 8px;\n border-radius: 999px;\n }\n\n .instructions,\n .language-wrap label,\n .words-title {\n font-size: 12px;\n color: #6b7280;\n }\n\n .language-wrap {\n display: flex;\n align-items: center;\n gap: 6px;\n }\n\n .language-wrap select {\n font-size: 12px;\n padding: 4px 8px;\n border-radius: 6px;\n border: 1px solid #d4d4d8;\n background: #fff;\n }\n\n .buttons-wrap {\n display: flex;\n gap: 8px;\n }\n\n .btn {\n border-radius: 8px;\n font-size: 12px;\n padding: 6px 10px;\n cursor: pointer;\n border: 1px solid #d4d4d8;\n }\n\n .btn-secondary {\n background: #fff;\n color: #374151;\n }\n\n .btn-primary {\n background: #0ea5e9;\n color: #fff;\n border-color: #0ea5e9;\n }\n\n .board-wrapper {\n width: 100%;\n display: flex;\n justify-content: center;\n }\n\n .board-grid {\n width: 100%;\n aspect-ratio: 1 / 1;\n display: grid;\n gap: 1px;\n user-select: none;\n touch-action: none;\n }\n\n .word-search-cell {\n display: inline-flex;\n width: 100%;\n height: 100%;\n min-width: 0;\n align-items: center;\n justify-content: center;\n border: 1px solid #d4d4d8;\n border-radius: 8px;\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n font-weight: 700;\n font-size: clamp(12px, 1.8vw, 16px);\n color: #111827;\n background: #fff;\n transition: background 120ms ease;\n }\n\n .path-cell {\n background: #e0f2fe;\n }\n\n .found-cell {\n background: #bae6fd;\n }\n\n .words-section {\n margin-top: 14px;\n display: grid;\n gap: 8px;\n }\n\n .words-list {\n display: flex;\n gap: 8px;\n flex-wrap: wrap;\n }\n\n .word-chip {\n display: inline-flex;\n align-items: center;\n padding: 6px 10px;\n border-radius: 999px;\n border: 1px solid #d4d4d8;\n background: #fafafa;\n color: #3f3f46;\n font-size: 12px;\n font-weight: 600;\n letter-spacing: 0.04em;\n }\n\n .word-chip-found {\n border-color: #22c55e;\n background: #dcfce7;\n color: #166534;\n text-decoration: line-through;\n text-decoration-thickness: 2px;\n }\n\n .all-found {\n font-size: 12px;\n color: #16a34a;\n font-weight: 600;\n }\n\n .completion-card {\n margin-top: 14px;\n border: 1px solid #86efac;\n background: linear-gradient(180deg, #f0fdf4 0%, #dcfce7 100%);\n border-radius: 12px;\n padding: 12px;\n }\n\n .completion-title {\n font-size: 14px;\n font-weight: 700;\n color: #166534;\n margin-bottom: 4px;\n }\n\n .completion-message {\n font-size: 12px;\n color: #166534;\n margin-bottom: 10px;\n }\n\n .completion-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));\n gap: 8px;\n font-size: 12px;\n color: #14532d;\n }\n\n @keyframes pulse {\n 0% {\n opacity: 1;\n }\n 100% {\n opacity: 0;\n }\n }\n\n @keyframes bounce {\n 0%,\n 100% {\n transform: translate(-50%, -50%) scale(1);\n }\n 50% {\n transform: translate(-50%, -50%) scale(1.1);\n }\n }\n `\n ]\n})\nexport class WordSearchComponent implements OnInit, OnChanges, OnDestroy {\n @Input({ required: true }) rows = 10;\n @Input({ required: true }) cols = 10;\n @Input({ required: true }) words: string[] = [];\n @Input() allowDiagonal = true;\n @Output() completed = new EventEmitter<WordSearchCompletionReport>();\n @Output() generated = new EventEmitter<WordSearchPuzzle>();\n\n puzzle: WordSearchPuzzle | null = null;\n language: LanguageCode = \"EN\";\n elapsedSeconds = 0;\n showCelebration = false;\n completionReport: WordSearchCompletionReport | null = null;\n\n foundWords = new Set<string>();\n normalizedInputWords: string[] = [];\n pendingWords: string[] = [];\n foundCellKeys = new Set<string>();\n currentPathKeys = new Set<string>();\n\n isDragging = false;\n dragStart: CellPosition | null = null;\n dragCurrent: CellPosition | null = null;\n\n puzzleSeed = 0;\n boardSize = 280;\n\n private timerHandle: ReturnType<typeof setInterval> | null = null;\n private celebrationTimeout: ReturnType<typeof setTimeout> | null = null;\n\n get t() {\n return TRANSLATIONS[this.language];\n }\n\n ngOnInit(): void {\n this.timerHandle = setInterval(() => {\n this.elapsedSeconds += 1;\n }, 1000);\n\n this.generatePuzzle();\n }\n\n ngOnChanges(): void {\n this.normalizedInputWords = this.words.map(normalizeWord).filter(Boolean);\n this.pendingWords = [...this.normalizedInputWords];\n this.boardSize = Math.min(560, Math.max(280, Math.max(this.rows, this.cols) * 38));\n\n if (!this.words.length) {\n this.puzzle = null;\n this.clearRoundState();\n return;\n }\n\n if (this.puzzle) {\n this.generatePuzzle();\n }\n }\n\n ngOnDestroy(): void {\n if (this.timerHandle) {\n clearInterval(this.timerHandle);\n }\n if (this.celebrationTimeout) {\n clearTimeout(this.celebrationTimeout);\n }\n }\n\n @HostListener(\"document:mouseup\")\n onDocumentMouseUp(): void {\n this.finishSelection();\n }\n\n setLanguage(value: LanguageCode): void {\n this.language = value;\n }\n\n formatTime(seconds: number): string {\n const hours = Math.floor(seconds / 3600);\n const minutes = Math.floor((seconds % 3600) / 60);\n const secs = seconds % 60;\n\n if (hours > 0) {\n return `${String(hours).padStart(2, \"0\")}:${String(minutes).padStart(2, \"0\")}:${String(secs).padStart(2, \"0\")}`;\n }\n\n return `${String(minutes).padStart(2, \"0\")}:${String(secs).padStart(2, \"0\")}`;\n }\n\n formatCompletedAt(timestamp: string): string {\n return new Date(timestamp).toLocaleTimeString();\n }\n\n isFoundCell(row: number, col: number): boolean {\n return this.foundCellKeys.has(`${row}:${col}`);\n }\n\n isPathCell(row: number, col: number): boolean {\n return this.currentPathKeys.has(`${row}:${col}`);\n }\n\n isWordFound(word: string): boolean {\n return this.foundWords.has(word);\n }\n\n startSelection(row: number, col: number): void {\n this.isDragging = true;\n this.dragStart = { row, col };\n this.dragCurrent = { row, col };\n this.refreshCurrentPathKeys();\n }\n\n moveSelection(row: number, col: number): void {\n if (!this.isDragging) {\n return;\n }\n\n this.dragCurrent = { row, col };\n this.refreshCurrentPathKeys();\n }\n\n finishSelection(): void {\n if (!this.isDragging || !this.dragStart || !this.dragCurrent || !this.puzzle) {\n this.clearDragState();\n return;\n }\n\n const path = buildPath(this.dragStart, this.dragCurrent);\n const match = this.puzzle.placements.find((placement) => matchesPlacement(path, placement));\n\n if (match && !this.foundWords.has(match.word)) {\n this.foundWords.add(match.word);\n this.refreshFoundCellKeys();\n this.pendingWords = this.normalizedInputWords.filter((word) => !this.foundWords.has(word));\n playSuccessSound();\n\n if (this.foundWords.size === this.normalizedInputWords.length) {\n const report: WordSearchCompletionReport = {\n elapsedSeconds: this.elapsedSeconds,\n foundWords: this.foundWords.size,\n totalWords: this.normalizedInputWords.length,\n rows: this.rows,\n cols: this.cols,\n allowDiagonal: this.allowDiagonal,\n completedAt: new Date().toISOString()\n };\n\n this.completionReport = report;\n this.completed.emit(report);\n playWinnerSound();\n this.showCelebration = true;\n if (this.celebrationTimeout) {\n clearTimeout(this.celebrationTimeout);\n }\n this.celebrationTimeout = setTimeout(() => {\n this.showCelebration = false;\n }, 3000);\n }\n }\n\n this.clearDragState();\n }\n\n resetProgress(): void {\n this.foundWords = new Set<string>();\n this.pendingWords = [...this.normalizedInputWords];\n this.foundCellKeys = new Set<string>();\n this.completionReport = null;\n this.showCelebration = false;\n this.clearDragState();\n }\n\n generateNewPuzzle(): void {\n this.puzzleSeed += 1;\n this.generatePuzzle();\n }\n\n private generatePuzzle(): void {\n if (this.words.length === 0) {\n this.puzzle = null;\n this.clearRoundState();\n return;\n }\n\n this.puzzle = createWordSearch({\n rows: this.rows,\n cols: this.cols,\n words: this.words,\n allowDiagonal: this.allowDiagonal\n });\n\n this.generated.emit(this.puzzle);\n this.elapsedSeconds = 0;\n this.resetProgress();\n }\n\n private refreshFoundCellKeys(): void {\n const next = new Set<string>();\n if (!this.puzzle) {\n this.foundCellKeys = next;\n return;\n }\n\n for (const placement of this.puzzle.placements) {\n if (!this.foundWords.has(placement.word)) {\n continue;\n }\n for (const cell of toPathFromPlacement(placement)) {\n next.add(toCellKey(cell));\n }\n }\n\n this.foundCellKeys = next;\n }\n\n private refreshCurrentPathKeys(): void {\n if (!this.dragStart || !this.dragCurrent) {\n this.currentPathKeys = new Set<string>();\n return;\n }\n\n const path = buildPath(this.dragStart, this.dragCurrent);\n const next = new Set<string>();\n for (const cell of path) {\n next.add(toCellKey(cell));\n }\n this.currentPathKeys = next;\n }\n\n private clearDragState(): void {\n this.isDragging = false;\n this.dragStart = null;\n this.dragCurrent = null;\n this.currentPathKeys = new Set<string>();\n }\n\n private clearRoundState(): void {\n this.elapsedSeconds = 0;\n this.showCelebration = false;\n this.completionReport = null;\n this.foundWords = new Set<string>();\n this.pendingWords = [...this.normalizedInputWords];\n this.foundCellKeys = new Set<string>();\n this.clearDragState();\n if (this.celebrationTimeout) {\n clearTimeout(this.celebrationTimeout);\n this.celebrationTimeout = null;\n }\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,130 @@
1
+ import { WordSearchOptions, WordSearchPuzzle } from '@js-gamifications/word-search-core';
2
+ export { WordSearchOptions, WordSearchPuzzle } from '@js-gamifications/word-search-core';
3
+ import * as i0 from '@angular/core';
4
+ import { OnInit, OnChanges, OnDestroy, EventEmitter } from '@angular/core';
5
+
6
+ declare class WordSearchService {
7
+ generate(options: WordSearchOptions): WordSearchPuzzle;
8
+ static ɵfac: i0.ɵɵFactoryDeclaration<WordSearchService, never>;
9
+ static ɵprov: i0.ɵɵInjectableDeclaration<WordSearchService>;
10
+ }
11
+
12
+ type LanguageCode = "EN" | "ES" | "AR";
13
+ interface CellPosition {
14
+ row: number;
15
+ col: number;
16
+ }
17
+ interface WordSearchCompletionReport {
18
+ elapsedSeconds: number;
19
+ foundWords: number;
20
+ totalWords: number;
21
+ rows: number;
22
+ cols: number;
23
+ allowDiagonal: boolean;
24
+ completedAt: string;
25
+ }
26
+ declare class WordSearchComponent implements OnInit, OnChanges, OnDestroy {
27
+ rows: number;
28
+ cols: number;
29
+ words: string[];
30
+ allowDiagonal: boolean;
31
+ completed: EventEmitter<WordSearchCompletionReport>;
32
+ generated: EventEmitter<WordSearchPuzzle>;
33
+ puzzle: WordSearchPuzzle | null;
34
+ language: LanguageCode;
35
+ elapsedSeconds: number;
36
+ showCelebration: boolean;
37
+ completionReport: WordSearchCompletionReport | null;
38
+ foundWords: Set<string>;
39
+ normalizedInputWords: string[];
40
+ pendingWords: string[];
41
+ foundCellKeys: Set<string>;
42
+ currentPathKeys: Set<string>;
43
+ isDragging: boolean;
44
+ dragStart: CellPosition | null;
45
+ dragCurrent: CellPosition | null;
46
+ puzzleSeed: number;
47
+ boardSize: number;
48
+ private timerHandle;
49
+ private celebrationTimeout;
50
+ get t(): {
51
+ readonly title: "Busqueda de Palabras";
52
+ readonly found: "encontradas";
53
+ readonly instructions: "Haz clic y arrastra sobre las letras para seleccionar una palabra.";
54
+ readonly clearProgress: "Reiniciar";
55
+ readonly newPuzzle: "Nuevo Juego";
56
+ readonly words: "Palabras";
57
+ readonly allWordsFound: "Todas las palabras encontradas";
58
+ readonly timer: "Tiempo";
59
+ readonly completionTitle: "Puzzle completado";
60
+ readonly completionMessage: "Excelente trabajo, encontraste todas las palabras.";
61
+ readonly completionTime: "Tiempo total";
62
+ readonly completionWords: "Palabras encontradas";
63
+ readonly completionGrid: "Tamano de tablero";
64
+ readonly completionDiagonal: "Diagonal habilitada";
65
+ readonly completionAt: "Completado a las";
66
+ readonly yes: "Si";
67
+ readonly no: "No";
68
+ } | {
69
+ readonly title: "Word Search";
70
+ readonly found: "found";
71
+ readonly instructions: "Hold click and drag over letters to select a word.";
72
+ readonly clearProgress: "Clear Progress";
73
+ readonly newPuzzle: "New Puzzle";
74
+ readonly words: "Words";
75
+ readonly allWordsFound: "All words found";
76
+ readonly timer: "Time";
77
+ readonly completionTitle: "Puzzle Completed";
78
+ readonly completionMessage: "Great job, you found every word.";
79
+ readonly completionTime: "Total time";
80
+ readonly completionWords: "Words found";
81
+ readonly completionGrid: "Grid size";
82
+ readonly completionDiagonal: "Diagonal enabled";
83
+ readonly completionAt: "Completed at";
84
+ readonly yes: "Yes";
85
+ readonly no: "No";
86
+ } | {
87
+ readonly title: "البحث عن الكلمات";
88
+ readonly found: "موجودة";
89
+ readonly instructions: "اضغط واسحب فوق الحروف لتحديد كلمة";
90
+ readonly clearProgress: "مسح التقدم";
91
+ readonly newPuzzle: "لعبة جديدة";
92
+ readonly words: "الكلمات";
93
+ readonly allWordsFound: "تم العثور على جميع الكلمات";
94
+ readonly timer: "الوقت";
95
+ readonly completionTitle: "اكتمل اللغز";
96
+ readonly completionMessage: "عمل رائع، لقد عثرت على جميع الكلمات";
97
+ readonly completionTime: "الوقت الكلي";
98
+ readonly completionWords: "الكلمات التي تم العثور عليها";
99
+ readonly completionGrid: "حجم الشبكة";
100
+ readonly completionDiagonal: "القطري مفعل";
101
+ readonly completionAt: "اكتمل في";
102
+ readonly yes: "نعم";
103
+ readonly no: "لا";
104
+ };
105
+ ngOnInit(): void;
106
+ ngOnChanges(): void;
107
+ ngOnDestroy(): void;
108
+ onDocumentMouseUp(): void;
109
+ setLanguage(value: LanguageCode): void;
110
+ formatTime(seconds: number): string;
111
+ formatCompletedAt(timestamp: string): string;
112
+ isFoundCell(row: number, col: number): boolean;
113
+ isPathCell(row: number, col: number): boolean;
114
+ isWordFound(word: string): boolean;
115
+ startSelection(row: number, col: number): void;
116
+ moveSelection(row: number, col: number): void;
117
+ finishSelection(): void;
118
+ resetProgress(): void;
119
+ generateNewPuzzle(): void;
120
+ private generatePuzzle;
121
+ private refreshFoundCellKeys;
122
+ private refreshCurrentPathKeys;
123
+ private clearDragState;
124
+ private clearRoundState;
125
+ static ɵfac: i0.ɵɵFactoryDeclaration<WordSearchComponent, never>;
126
+ static ɵcmp: i0.ɵɵComponentDeclaration<WordSearchComponent, "js-gamifications-word-search", never, { "rows": { "alias": "rows"; "required": true; }; "cols": { "alias": "cols"; "required": true; }; "words": { "alias": "words"; "required": true; }; "allowDiagonal": { "alias": "allowDiagonal"; "required": false; }; }, { "completed": "completed"; "generated": "generated"; }, never, never, true, never>;
127
+ }
128
+
129
+ export { WordSearchComponent, WordSearchService };
130
+ export type { WordSearchCompletionReport };
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@js-gamifications/word-search-angular",
3
+ "version": "1.0.1",
4
+ "description": "Angular bindings for word search puzzles",
5
+ "license": "MIT",
6
+ "main": "./dist/fesm2022/js-gamifications-word-search-angular.mjs",
7
+ "module": "./dist/fesm2022/js-gamifications-word-search-angular.mjs",
8
+ "types": "./dist/index.d.ts",
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/fesm2022/js-gamifications-word-search-angular.mjs"
16
+ }
17
+ },
18
+ "dependencies": {
19
+ "@js-gamifications/word-search-core": "^1.0.1"
20
+ },
21
+ "peerDependencies": {
22
+ "@angular/common": ">=17",
23
+ "@angular/core": ">=17",
24
+ "@angular/forms": ">=17",
25
+ "rxjs": ">=7"
26
+ },
27
+ "devDependencies": {
28
+ "@angular/common": "^20.0.0",
29
+ "@angular/compiler": "^20.0.0",
30
+ "@angular/compiler-cli": "^20.0.0",
31
+ "@angular/core": "^20.0.0",
32
+ "@angular/forms": "^20.0.0",
33
+ "ng-packagr": "^20.0.1",
34
+ "rxjs": "^7.8.2",
35
+ "tslib": "^2.8.1"
36
+ },
37
+ "scripts": {
38
+ "build": "ng-packagr -p ng-package.json",
39
+ "clean": "rm -rf dist",
40
+ "typecheck": "ng-packagr -p ng-package.json",
41
+ "test": "vitest run --passWithNoTests",
42
+ "lint": "tsc --noEmit"
43
+ }
44
+ }