@moraby/app-launcher 0.1.8 → 2.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/dist/index.mjs CHANGED
@@ -259,8 +259,525 @@ function AppLauncher({
259
259
  ] })
260
260
  ] });
261
261
  }
262
+
263
+ // src/AppSettings.tsx
264
+ import { useState as useState4 } from "react";
265
+ import { FaTimes, FaPlus, FaEdit, FaTrash, FaDownload } from "react-icons/fa";
266
+
267
+ // src/AddAppForm.tsx
268
+ import { useState as useState3 } from "react";
269
+
270
+ // src/IconPicker.tsx
271
+ import { useState as useState2 } from "react";
272
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
273
+ var allIcons = Object.keys(iconMap);
274
+ function IconPicker({ selectedIcon, onSelect }) {
275
+ const [searchTerm, setSearchTerm] = useState2("");
276
+ const filteredIcons = allIcons.filter(
277
+ (name) => name.toLowerCase().includes(searchTerm.toLowerCase())
278
+ );
279
+ return /* @__PURE__ */ jsxs2("div", { className: "app-launcher-icon-picker", children: [
280
+ /* @__PURE__ */ jsx2(
281
+ "input",
282
+ {
283
+ type: "text",
284
+ placeholder: "Search icons...",
285
+ value: searchTerm,
286
+ onChange: (e) => setSearchTerm(e.target.value),
287
+ className: "app-launcher-icon-picker__search"
288
+ }
289
+ ),
290
+ /* @__PURE__ */ jsx2("div", { className: "app-launcher-icon-picker__grid", children: filteredIcons.map((iconName) => {
291
+ const Icon = iconMap[iconName];
292
+ const isSelected = selectedIcon === iconName;
293
+ return /* @__PURE__ */ jsx2(
294
+ "button",
295
+ {
296
+ className: `app-launcher-icon-picker__button ${isSelected ? "app-launcher-icon-picker__button--selected" : ""}`,
297
+ onClick: () => onSelect(iconName),
298
+ title: iconName,
299
+ type: "button",
300
+ children: /* @__PURE__ */ jsx2(Icon, { className: "app-launcher-icon-picker__icon" })
301
+ },
302
+ iconName
303
+ );
304
+ }) }),
305
+ filteredIcons.length === 0 && /* @__PURE__ */ jsx2("div", { className: "app-launcher-icon-picker__empty", children: "No icons found" })
306
+ ] });
307
+ }
308
+
309
+ // src/AddAppForm.tsx
310
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
311
+ var presetColors = [
312
+ "#4285F4",
313
+ // Blue
314
+ "#EA4335",
315
+ // Red
316
+ "#FBBC04",
317
+ // Yellow
318
+ "#34A853",
319
+ // Green
320
+ "#FF6D01",
321
+ // Orange
322
+ "#46BDC6",
323
+ // Teal
324
+ "#7B1FA2",
325
+ // Purple
326
+ "#E91E63",
327
+ // Pink
328
+ "#607D8B",
329
+ // Gray
330
+ "#795548"
331
+ // Brown
332
+ ];
333
+ function AddAppForm({
334
+ initialData,
335
+ onSubmit,
336
+ onCancel,
337
+ submitLabel = "Add App"
338
+ }) {
339
+ const [name, setName] = useState3(initialData?.name || "");
340
+ const [url, setUrl] = useState3(initialData?.url || "");
341
+ const initialIcon = initialData?.icon || "FaRocket";
342
+ const isInitialCustom = initialIcon.startsWith("/") || initialIcon.startsWith("http");
343
+ const [iconName, setIconName] = useState3(isInitialCustom ? "FaRocket" : initialIcon);
344
+ const [customIconUrl, setCustomIconUrl] = useState3(isInitialCustom ? initialIcon : "");
345
+ const [iconMode, setIconMode] = useState3(isInitialCustom ? "custom" : "picker");
346
+ const [color, setColor] = useState3(initialData?.color || "#4285F4");
347
+ const [description, setDescription] = useState3(initialData?.description || "");
348
+ const [errors, setErrors] = useState3({});
349
+ const SelectedIcon = iconMap[iconName] || iconMap["FaRocket"];
350
+ const validate = () => {
351
+ const newErrors = {};
352
+ if (!name.trim()) {
353
+ newErrors.name = "Name is required";
354
+ }
355
+ if (!url.trim()) {
356
+ newErrors.url = "URL is required";
357
+ } else if (!url.startsWith("http://") && !url.startsWith("https://") && !url.startsWith("/")) {
358
+ newErrors.url = "URL must start with http://, https://, or /";
359
+ }
360
+ if (iconMode === "custom" && customIconUrl.trim()) {
361
+ if (!customIconUrl.startsWith("http://") && !customIconUrl.startsWith("https://") && !customIconUrl.startsWith("/")) {
362
+ newErrors.customIconUrl = "Icon URL must start with http://, https://, or /";
363
+ }
364
+ }
365
+ setErrors(newErrors);
366
+ return Object.keys(newErrors).length === 0;
367
+ };
368
+ const handleSubmit = (e) => {
369
+ e.preventDefault();
370
+ if (!validate()) return;
371
+ const finalIcon = iconMode === "custom" && customIconUrl.trim() ? customIconUrl.trim() : iconName;
372
+ onSubmit({
373
+ name: name.trim(),
374
+ url: url.trim(),
375
+ icon: finalIcon,
376
+ color,
377
+ description: description.trim() || void 0
378
+ });
379
+ };
380
+ return /* @__PURE__ */ jsxs3("form", { onSubmit: handleSubmit, className: "app-launcher-form", children: [
381
+ /* @__PURE__ */ jsxs3("div", { className: "app-launcher-form__preview", children: [
382
+ /* @__PURE__ */ jsx3("div", { className: "app-launcher-form__preview-icon", style: { backgroundColor: color + "20" }, children: iconMode === "custom" && customIconUrl ? customIconUrl.endsWith(".svg") ? /* @__PURE__ */ jsx3(
383
+ "div",
384
+ {
385
+ className: "app-launcher-form__preview-svg",
386
+ style: {
387
+ maskImage: `url(${customIconUrl})`,
388
+ WebkitMaskImage: `url(${customIconUrl})`,
389
+ backgroundColor: color === "transparent" ? "#5f6368" : color
390
+ }
391
+ }
392
+ ) : /* @__PURE__ */ jsx3(
393
+ "img",
394
+ {
395
+ src: customIconUrl,
396
+ alt: "Icon preview",
397
+ className: "app-launcher-form__preview-img"
398
+ }
399
+ ) : /* @__PURE__ */ jsx3(SelectedIcon, { style: { color, fontSize: 32 } }) }),
400
+ /* @__PURE__ */ jsx3("span", { className: "app-launcher-form__preview-name", children: name || "App Name" })
401
+ ] }),
402
+ /* @__PURE__ */ jsxs3("div", { className: "app-launcher-form__field", children: [
403
+ /* @__PURE__ */ jsx3("label", { htmlFor: "app-name", className: "app-launcher-form__label", children: "Name *" }),
404
+ /* @__PURE__ */ jsx3(
405
+ "input",
406
+ {
407
+ id: "app-name",
408
+ type: "text",
409
+ value: name,
410
+ onChange: (e) => setName(e.target.value),
411
+ placeholder: "My App",
412
+ className: `app-launcher-form__input ${errors.name ? "app-launcher-form__input--error" : ""}`
413
+ }
414
+ ),
415
+ errors.name && /* @__PURE__ */ jsx3("span", { className: "app-launcher-form__error", children: errors.name })
416
+ ] }),
417
+ /* @__PURE__ */ jsxs3("div", { className: "app-launcher-form__field", children: [
418
+ /* @__PURE__ */ jsx3("label", { htmlFor: "app-url", className: "app-launcher-form__label", children: "URL *" }),
419
+ /* @__PURE__ */ jsx3(
420
+ "input",
421
+ {
422
+ id: "app-url",
423
+ type: "text",
424
+ value: url,
425
+ onChange: (e) => setUrl(e.target.value),
426
+ placeholder: "https://myapp.com or /dashboard",
427
+ className: `app-launcher-form__input ${errors.url ? "app-launcher-form__input--error" : ""}`
428
+ }
429
+ ),
430
+ errors.url && /* @__PURE__ */ jsx3("span", { className: "app-launcher-form__error", children: errors.url })
431
+ ] }),
432
+ /* @__PURE__ */ jsxs3("div", { className: "app-launcher-form__field", children: [
433
+ /* @__PURE__ */ jsx3("label", { htmlFor: "app-description", className: "app-launcher-form__label", children: "Description" }),
434
+ /* @__PURE__ */ jsx3(
435
+ "input",
436
+ {
437
+ id: "app-description",
438
+ type: "text",
439
+ value: description,
440
+ onChange: (e) => setDescription(e.target.value),
441
+ placeholder: "Optional description (shown on hover)",
442
+ className: "app-launcher-form__input"
443
+ }
444
+ )
445
+ ] }),
446
+ /* @__PURE__ */ jsxs3("div", { className: "app-launcher-form__field", children: [
447
+ /* @__PURE__ */ jsx3("label", { className: "app-launcher-form__label", children: "Color" }),
448
+ /* @__PURE__ */ jsxs3("div", { className: "app-launcher-form__color-picker", children: [
449
+ presetColors.map((presetColor) => /* @__PURE__ */ jsx3(
450
+ "button",
451
+ {
452
+ type: "button",
453
+ className: `app-launcher-form__color-button ${color === presetColor ? "app-launcher-form__color-button--selected" : ""}`,
454
+ style: { backgroundColor: presetColor },
455
+ onClick: () => setColor(presetColor),
456
+ title: presetColor
457
+ },
458
+ presetColor
459
+ )),
460
+ /* @__PURE__ */ jsx3(
461
+ "input",
462
+ {
463
+ type: "color",
464
+ value: color,
465
+ onChange: (e) => setColor(e.target.value),
466
+ className: "app-launcher-form__color-input",
467
+ title: "Custom color"
468
+ }
469
+ )
470
+ ] })
471
+ ] }),
472
+ /* @__PURE__ */ jsxs3("div", { className: "app-launcher-form__field", children: [
473
+ /* @__PURE__ */ jsx3("label", { className: "app-launcher-form__label", children: "Icon" }),
474
+ /* @__PURE__ */ jsxs3("div", { className: "app-launcher-form__icon-mode", children: [
475
+ /* @__PURE__ */ jsx3(
476
+ "button",
477
+ {
478
+ type: "button",
479
+ className: `app-launcher-form__mode-button ${iconMode === "picker" ? "app-launcher-form__mode-button--active" : ""}`,
480
+ onClick: () => setIconMode("picker"),
481
+ children: "Icon Picker"
482
+ }
483
+ ),
484
+ /* @__PURE__ */ jsx3(
485
+ "button",
486
+ {
487
+ type: "button",
488
+ className: `app-launcher-form__mode-button ${iconMode === "custom" ? "app-launcher-form__mode-button--active" : ""}`,
489
+ onClick: () => setIconMode("custom"),
490
+ children: "Custom URL"
491
+ }
492
+ )
493
+ ] }),
494
+ iconMode === "picker" ? /* @__PURE__ */ jsx3(IconPicker, { selectedIcon: iconName, onSelect: setIconName }) : /* @__PURE__ */ jsxs3("div", { className: "app-launcher-form__custom-icon", children: [
495
+ /* @__PURE__ */ jsx3(
496
+ "input",
497
+ {
498
+ id: "app-custom-icon",
499
+ type: "text",
500
+ value: customIconUrl,
501
+ onChange: (e) => setCustomIconUrl(e.target.value),
502
+ placeholder: "/icons/my-icon.svg or https://cdn.example.com/icon.svg",
503
+ className: `app-launcher-form__input ${errors.customIconUrl ? "app-launcher-form__input--error" : ""}`
504
+ }
505
+ ),
506
+ errors.customIconUrl && /* @__PURE__ */ jsx3("span", { className: "app-launcher-form__error", children: errors.customIconUrl }),
507
+ /* @__PURE__ */ jsx3("p", { className: "app-launcher-form__hint", children: "Enter a path from your public folder (e.g., /icons/logo.svg) or a full URL" })
508
+ ] })
509
+ ] }),
510
+ /* @__PURE__ */ jsxs3("div", { className: "app-launcher-form__actions", children: [
511
+ /* @__PURE__ */ jsx3(
512
+ "button",
513
+ {
514
+ type: "button",
515
+ onClick: onCancel,
516
+ className: "app-launcher-form__button app-launcher-form__button--cancel",
517
+ children: "Cancel"
518
+ }
519
+ ),
520
+ /* @__PURE__ */ jsx3(
521
+ "button",
522
+ {
523
+ type: "submit",
524
+ className: "app-launcher-form__button app-launcher-form__button--submit",
525
+ children: submitLabel
526
+ }
527
+ )
528
+ ] })
529
+ ] });
530
+ }
531
+
532
+ // src/AppSettings.tsx
533
+ import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
534
+ function AppSettings({
535
+ isOpen,
536
+ onClose,
537
+ apps,
538
+ defaultApps = [],
539
+ onAdd,
540
+ onUpdate,
541
+ onDelete
542
+ }) {
543
+ const [showAddForm, setShowAddForm] = useState4(false);
544
+ const [editingApp, setEditingApp] = useState4(null);
545
+ if (!isOpen) return null;
546
+ const handleAddSubmit = (data) => {
547
+ onAdd(data);
548
+ setShowAddForm(false);
549
+ };
550
+ const handleEditSubmit = (data) => {
551
+ if (editingApp) {
552
+ onUpdate(editingApp.id, data);
553
+ setEditingApp(null);
554
+ }
555
+ };
556
+ const handleDelete = (id) => {
557
+ if (confirm("Are you sure you want to delete this app?")) {
558
+ onDelete(id);
559
+ }
560
+ };
561
+ const handleExportConfig = () => {
562
+ const exportedApps = [
563
+ // User apps first (they appear at top)
564
+ ...apps,
565
+ // Then default apps
566
+ ...defaultApps.filter((da) => !apps.some((ua) => ua.id === da.id))
567
+ // Avoid duplicates if any
568
+ ];
569
+ const config = {
570
+ version: "1.0",
571
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
572
+ apps: exportedApps
573
+ };
574
+ const blob = new Blob([JSON.stringify(config, null, 2)], { type: "application/json" });
575
+ const url = URL.createObjectURL(blob);
576
+ const a = document.createElement("a");
577
+ a.href = url;
578
+ a.download = "app-launcher-config.json";
579
+ document.body.appendChild(a);
580
+ a.click();
581
+ document.body.removeChild(a);
582
+ URL.revokeObjectURL(url);
583
+ };
584
+ return /* @__PURE__ */ jsx4("div", { className: "app-settings__overlay", children: /* @__PURE__ */ jsxs4("div", { className: "app-settings__modal", onClick: (e) => e.stopPropagation(), children: [
585
+ /* @__PURE__ */ jsxs4("div", { className: "app-settings__header", children: [
586
+ /* @__PURE__ */ jsx4("h2", { className: "app-settings__title", children: showAddForm ? "Add New App" : editingApp ? "Edit App" : "Settings" }),
587
+ /* @__PURE__ */ jsx4("button", { className: "app-settings__close-button", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsx4(FaTimes, {}) })
588
+ ] }),
589
+ /* @__PURE__ */ jsx4("div", { className: "app-settings__content", children: showAddForm ? /* @__PURE__ */ jsx4(
590
+ AddAppForm,
591
+ {
592
+ onSubmit: handleAddSubmit,
593
+ onCancel: () => setShowAddForm(false),
594
+ submitLabel: "Add App"
595
+ }
596
+ ) : editingApp ? /* @__PURE__ */ jsx4(
597
+ AddAppForm,
598
+ {
599
+ initialData: editingApp,
600
+ onSubmit: handleEditSubmit,
601
+ onCancel: () => setEditingApp(null),
602
+ submitLabel: "Save Changes"
603
+ }
604
+ ) : /* @__PURE__ */ jsxs4(Fragment, { children: [
605
+ /* @__PURE__ */ jsxs4("button", { className: "app-settings__add-button", onClick: () => setShowAddForm(true), children: [
606
+ /* @__PURE__ */ jsx4(FaPlus, { className: "app-settings__add-icon" }),
607
+ "Add New App"
608
+ ] }),
609
+ /* @__PURE__ */ jsxs4("div", { className: "app-settings__list", children: [
610
+ /* @__PURE__ */ jsx4("h3", { className: "app-settings__section-title", children: "My Custom Apps" }),
611
+ apps.length === 0 ? /* @__PURE__ */ jsx4("p", { className: "app-settings__empty-message", children: 'No custom apps yet. Click "Add New App" to get started.' }) : /* @__PURE__ */ jsx4("div", { className: "app-settings__grid", children: apps.map((app) => {
612
+ const isCustom = app.icon.startsWith("/") || app.icon.startsWith("http");
613
+ const Icon = !isCustom ? iconMap[app.icon] || iconMap["FaRocket"] : null;
614
+ return /* @__PURE__ */ jsxs4("div", { className: "app-settings__card", children: [
615
+ /* @__PURE__ */ jsxs4("div", { className: "app-settings__card-info", children: [
616
+ /* @__PURE__ */ jsx4(
617
+ "div",
618
+ {
619
+ className: "app-settings__card-icon",
620
+ style: { backgroundColor: app.color + "20" },
621
+ children: isCustom ? app.icon.endsWith(".svg") ? /* @__PURE__ */ jsx4(
622
+ "div",
623
+ {
624
+ style: {
625
+ width: 20,
626
+ height: 20,
627
+ maskImage: `url(${app.icon})`,
628
+ WebkitMaskImage: `url(${app.icon})`,
629
+ maskSize: "contain",
630
+ maskRepeat: "no-repeat",
631
+ maskPosition: "center",
632
+ WebkitMaskSize: "contain",
633
+ WebkitMaskRepeat: "no-repeat",
634
+ WebkitMaskPosition: "center",
635
+ backgroundColor: app.color === "transparent" ? "#5f6368" : app.color
636
+ }
637
+ }
638
+ ) : /* @__PURE__ */ jsx4(
639
+ "img",
640
+ {
641
+ src: app.icon,
642
+ alt: app.name,
643
+ style: { width: 20, height: 20, objectFit: "contain" }
644
+ }
645
+ ) : Icon && /* @__PURE__ */ jsx4(Icon, { style: { color: app.color } })
646
+ }
647
+ ),
648
+ /* @__PURE__ */ jsxs4("div", { className: "app-settings__card-details", children: [
649
+ /* @__PURE__ */ jsx4("span", { className: "app-settings__card-name", children: app.name }),
650
+ /* @__PURE__ */ jsx4("span", { className: "app-settings__card-url", children: app.url })
651
+ ] })
652
+ ] }),
653
+ /* @__PURE__ */ jsxs4("div", { className: "app-settings__card-actions", children: [
654
+ /* @__PURE__ */ jsx4(
655
+ "button",
656
+ {
657
+ className: "app-settings__action-button",
658
+ onClick: () => setEditingApp(app),
659
+ title: "Edit",
660
+ children: /* @__PURE__ */ jsx4(FaEdit, {})
661
+ }
662
+ ),
663
+ /* @__PURE__ */ jsx4(
664
+ "button",
665
+ {
666
+ className: "app-settings__action-button app-settings__action-button--delete",
667
+ onClick: () => handleDelete(app.id),
668
+ title: "Delete",
669
+ children: /* @__PURE__ */ jsx4(FaTrash, {})
670
+ }
671
+ )
672
+ ] })
673
+ ] }, app.id);
674
+ }) })
675
+ ] }),
676
+ /* @__PURE__ */ jsxs4("button", { className: "app-settings__export-button", onClick: handleExportConfig, children: [
677
+ /* @__PURE__ */ jsx4(FaDownload, { className: "app-settings__export-icon" }),
678
+ "Export Configuration"
679
+ ] }),
680
+ /* @__PURE__ */ jsx4("div", { className: "app-settings__info", children: /* @__PURE__ */ jsx4("p", { children: 'Custom apps are saved in your browser. Use "Export Configuration" to share with other apps.' }) })
681
+ ] }) })
682
+ ] }) });
683
+ }
684
+
685
+ // src/LocalAppLauncher.tsx
686
+ import { useState as useState5, useEffect as useEffect2 } from "react";
687
+ import { FaCog as FaCog2 } from "react-icons/fa";
688
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
689
+ var STORAGE_KEY = "app-launcher-user-apps";
690
+ function LocalAppLauncher({
691
+ defaultApps = [],
692
+ className,
693
+ mergeDefaultApps = true
694
+ }) {
695
+ const [showSettings, setShowSettings] = useState5(false);
696
+ const [userApps, setUserApps] = useState5([]);
697
+ const [isLoaded, setIsLoaded] = useState5(false);
698
+ useEffect2(() => {
699
+ if (typeof window !== "undefined") {
700
+ try {
701
+ const stored = localStorage.getItem(STORAGE_KEY);
702
+ if (stored) {
703
+ setUserApps(JSON.parse(stored));
704
+ }
705
+ } catch (e) {
706
+ console.error("Failed to load apps from localStorage", e);
707
+ }
708
+ setIsLoaded(true);
709
+ }
710
+ }, []);
711
+ const saveApps = (apps) => {
712
+ setUserApps(apps);
713
+ if (typeof window !== "undefined") {
714
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(apps));
715
+ }
716
+ };
717
+ const handleAddApp = (data) => {
718
+ const newApp = {
719
+ ...data,
720
+ id: `custom-${Date.now()}`
721
+ };
722
+ saveApps([...userApps, newApp]);
723
+ };
724
+ const handleUpdateApp = (id, updates) => {
725
+ saveApps(userApps.map((app) => app.id === id ? { ...app, ...updates } : app));
726
+ };
727
+ const handleDeleteApp = (id) => {
728
+ saveApps(userApps.filter((app) => app.id !== id));
729
+ };
730
+ const displayApps = isLoaded ? mergeDefaultApps ? [...userApps, ...defaultApps.filter((da) => !userApps.some((ua) => ua.id === da.id))] : userApps.length > 0 ? userApps : defaultApps : [];
731
+ return /* @__PURE__ */ jsxs5(Fragment2, { children: [
732
+ /* @__PURE__ */ jsx5(
733
+ AppLauncher,
734
+ {
735
+ apps: displayApps,
736
+ className,
737
+ renderFooter: () => /* @__PURE__ */ jsxs5(
738
+ "button",
739
+ {
740
+ className: "app-launcher__footer-button",
741
+ style: {
742
+ display: "flex",
743
+ alignItems: "center",
744
+ gap: "8px",
745
+ width: "100%",
746
+ justifyContent: "center",
747
+ border: "none",
748
+ background: "transparent",
749
+ cursor: "pointer",
750
+ color: "var(--al-text-secondary)",
751
+ fontSize: "14px",
752
+ fontWeight: 500
753
+ },
754
+ onClick: () => setShowSettings(true),
755
+ children: [
756
+ /* @__PURE__ */ jsx5(FaCog2, { style: { fontSize: "16px" } }),
757
+ "Settings"
758
+ ]
759
+ }
760
+ )
761
+ }
762
+ ),
763
+ /* @__PURE__ */ jsx5(
764
+ AppSettings,
765
+ {
766
+ isOpen: showSettings,
767
+ onClose: () => setShowSettings(false),
768
+ apps: userApps,
769
+ defaultApps,
770
+ onAdd: handleAddApp,
771
+ onUpdate: handleUpdateApp,
772
+ onDelete: handleDeleteApp
773
+ }
774
+ )
775
+ ] });
776
+ }
262
777
  export {
263
778
  AppLauncher,
779
+ AppSettings,
780
+ LocalAppLauncher,
264
781
  AppLauncher as default,
265
782
  getIcon,
266
783
  iconMap