new structure

This commit is contained in:
2026-03-20 01:11:32 +01:00
parent d24bad018e
commit 4b4eaa083b
76 changed files with 578 additions and 72 deletions

View File

@@ -0,0 +1,275 @@
import {
useMediaQuery
} from "./chunk-RLEUDPPB.js";
import {
computed,
ref,
shallowRef,
watch
} from "./chunk-3S55Y3P7.js";
// node_modules/vitepress/dist/client/theme-default/index.js
import "/config/workspace/sigpro/node_modules/vitepress/dist/client/theme-default/styles/fonts.css";
// node_modules/vitepress/dist/client/theme-default/without-fonts.js
import "/config/workspace/sigpro/node_modules/vitepress/dist/client/theme-default/styles/vars.css";
import "/config/workspace/sigpro/node_modules/vitepress/dist/client/theme-default/styles/base.css";
import "/config/workspace/sigpro/node_modules/vitepress/dist/client/theme-default/styles/icons.css";
import "/config/workspace/sigpro/node_modules/vitepress/dist/client/theme-default/styles/utils.css";
import "/config/workspace/sigpro/node_modules/vitepress/dist/client/theme-default/styles/components/custom-block.css";
import "/config/workspace/sigpro/node_modules/vitepress/dist/client/theme-default/styles/components/vp-code.css";
import "/config/workspace/sigpro/node_modules/vitepress/dist/client/theme-default/styles/components/vp-code-group.css";
import "/config/workspace/sigpro/node_modules/vitepress/dist/client/theme-default/styles/components/vp-doc.css";
import "/config/workspace/sigpro/node_modules/vitepress/dist/client/theme-default/styles/components/vp-sponsor.css";
import VPBadge from "/config/workspace/sigpro/node_modules/vitepress/dist/client/theme-default/components/VPBadge.vue";
import Layout from "/config/workspace/sigpro/node_modules/vitepress/dist/client/theme-default/Layout.vue";
import { default as default2 } from "/config/workspace/sigpro/node_modules/vitepress/dist/client/theme-default/components/VPBadge.vue";
import { default as default3 } from "/config/workspace/sigpro/node_modules/vitepress/dist/client/theme-default/components/VPButton.vue";
import { default as default4 } from "/config/workspace/sigpro/node_modules/vitepress/dist/client/theme-default/components/VPDocAsideSponsors.vue";
import { default as default5 } from "/config/workspace/sigpro/node_modules/vitepress/dist/client/theme-default/components/VPFeatures.vue";
import { default as default6 } from "/config/workspace/sigpro/node_modules/vitepress/dist/client/theme-default/components/VPHomeContent.vue";
import { default as default7 } from "/config/workspace/sigpro/node_modules/vitepress/dist/client/theme-default/components/VPHomeFeatures.vue";
import { default as default8 } from "/config/workspace/sigpro/node_modules/vitepress/dist/client/theme-default/components/VPHomeHero.vue";
import { default as default9 } from "/config/workspace/sigpro/node_modules/vitepress/dist/client/theme-default/components/VPHomeSponsors.vue";
import { default as default10 } from "/config/workspace/sigpro/node_modules/vitepress/dist/client/theme-default/components/VPImage.vue";
import { default as default11 } from "/config/workspace/sigpro/node_modules/vitepress/dist/client/theme-default/components/VPLink.vue";
import { default as default12 } from "/config/workspace/sigpro/node_modules/vitepress/dist/client/theme-default/components/VPNavBarSearch.vue";
import { default as default13 } from "/config/workspace/sigpro/node_modules/vitepress/dist/client/theme-default/components/VPSocialLink.vue";
import { default as default14 } from "/config/workspace/sigpro/node_modules/vitepress/dist/client/theme-default/components/VPSocialLinks.vue";
import { default as default15 } from "/config/workspace/sigpro/node_modules/vitepress/dist/client/theme-default/components/VPSponsors.vue";
import { default as default16 } from "/config/workspace/sigpro/node_modules/vitepress/dist/client/theme-default/components/VPTeamMembers.vue";
import { default as default17 } from "/config/workspace/sigpro/node_modules/vitepress/dist/client/theme-default/components/VPTeamPage.vue";
import { default as default18 } from "/config/workspace/sigpro/node_modules/vitepress/dist/client/theme-default/components/VPTeamPageSection.vue";
import { default as default19 } from "/config/workspace/sigpro/node_modules/vitepress/dist/client/theme-default/components/VPTeamPageTitle.vue";
// node_modules/vitepress/dist/client/theme-default/composables/local-nav.js
import { onContentUpdated } from "vitepress";
// node_modules/vitepress/dist/client/theme-default/composables/outline.js
import { getScrollOffset } from "vitepress";
// node_modules/vitepress/dist/client/theme-default/support/utils.js
import { withBase } from "vitepress";
// node_modules/vitepress/dist/client/theme-default/composables/data.js
import { useData as useData$ } from "vitepress";
var useData = useData$;
// node_modules/vitepress/dist/client/theme-default/support/utils.js
function ensureStartingSlash(path) {
return path.startsWith("/") ? path : `/${path}`;
}
// node_modules/vitepress/dist/client/theme-default/support/sidebar.js
function getSidebar(_sidebar, path) {
if (Array.isArray(_sidebar))
return addBase(_sidebar);
if (_sidebar == null)
return [];
path = ensureStartingSlash(path);
const dir = Object.keys(_sidebar).sort((a, b) => {
return b.split("/").length - a.split("/").length;
}).find((dir2) => {
return path.startsWith(ensureStartingSlash(dir2));
});
const sidebar = dir ? _sidebar[dir] : [];
return Array.isArray(sidebar) ? addBase(sidebar) : addBase(sidebar.items, sidebar.base);
}
function getSidebarGroups(sidebar) {
const groups = [];
let lastGroupIndex = 0;
for (const index in sidebar) {
const item = sidebar[index];
if (item.items) {
lastGroupIndex = groups.push(item);
continue;
}
if (!groups[lastGroupIndex]) {
groups.push({ items: [] });
}
groups[lastGroupIndex].items.push(item);
}
return groups;
}
function addBase(items, _base) {
return [...items].map((_item) => {
const item = { ..._item };
const base = item.base || _base;
if (base && item.link)
item.link = base + item.link;
if (item.items)
item.items = addBase(item.items, base);
return item;
});
}
// node_modules/vitepress/dist/client/theme-default/composables/sidebar.js
function useSidebar() {
const { frontmatter, page, theme: theme2 } = useData();
const is960 = useMediaQuery("(min-width: 960px)");
const isOpen = ref(false);
const _sidebar = computed(() => {
const sidebarConfig = theme2.value.sidebar;
const relativePath = page.value.relativePath;
return sidebarConfig ? getSidebar(sidebarConfig, relativePath) : [];
});
const sidebar = ref(_sidebar.value);
watch(_sidebar, (next, prev) => {
if (JSON.stringify(next) !== JSON.stringify(prev))
sidebar.value = _sidebar.value;
});
const hasSidebar = computed(() => {
return frontmatter.value.sidebar !== false && sidebar.value.length > 0 && frontmatter.value.layout !== "home";
});
const leftAside = computed(() => {
if (hasAside)
return frontmatter.value.aside == null ? theme2.value.aside === "left" : frontmatter.value.aside === "left";
return false;
});
const hasAside = computed(() => {
if (frontmatter.value.layout === "home")
return false;
if (frontmatter.value.aside != null)
return !!frontmatter.value.aside;
return theme2.value.aside !== false;
});
const isSidebarEnabled = computed(() => hasSidebar.value && is960.value);
const sidebarGroups = computed(() => {
return hasSidebar.value ? getSidebarGroups(sidebar.value) : [];
});
function open() {
isOpen.value = true;
}
function close() {
isOpen.value = false;
}
function toggle() {
isOpen.value ? close() : open();
}
return {
isOpen,
sidebar,
sidebarGroups,
hasSidebar,
hasAside,
leftAside,
isSidebarEnabled,
open,
close,
toggle
};
}
// node_modules/vitepress/dist/client/theme-default/composables/outline.js
var ignoreRE = /\b(?:VPBadge|header-anchor|footnote-ref|ignore-header)\b/;
var resolvedHeaders = [];
function getHeaders(range) {
const headers = [
...document.querySelectorAll(".VPDoc :where(h1,h2,h3,h4,h5,h6)")
].filter((el) => el.id && el.hasChildNodes()).map((el) => {
const level = Number(el.tagName[1]);
return {
element: el,
title: serializeHeader(el),
link: "#" + el.id,
level
};
});
return resolveHeaders(headers, range);
}
function serializeHeader(h) {
let ret = "";
for (const node of h.childNodes) {
if (node.nodeType === 1) {
if (ignoreRE.test(node.className))
continue;
ret += node.textContent;
} else if (node.nodeType === 3) {
ret += node.textContent;
}
}
return ret.trim();
}
function resolveHeaders(headers, range) {
if (range === false) {
return [];
}
const levelsRange = (typeof range === "object" && !Array.isArray(range) ? range.level : range) || 2;
const [high, low] = typeof levelsRange === "number" ? [levelsRange, levelsRange] : levelsRange === "deep" ? [2, 6] : levelsRange;
return buildTree(headers, high, low);
}
function buildTree(data, min, max) {
resolvedHeaders.length = 0;
const result = [];
const stack = [];
data.forEach((item) => {
const node = { ...item, children: [] };
let parent = stack[stack.length - 1];
while (parent && parent.level >= node.level) {
stack.pop();
parent = stack[stack.length - 1];
}
if (node.element.classList.contains("ignore-header") || parent && "shouldIgnore" in parent) {
stack.push({ level: node.level, shouldIgnore: true });
return;
}
if (node.level > max || node.level < min)
return;
resolvedHeaders.push({ element: node.element, link: node.link });
if (parent)
parent.children.push(node);
else
result.push(node);
stack.push(node);
});
return result;
}
// node_modules/vitepress/dist/client/theme-default/composables/local-nav.js
function useLocalNav() {
const { theme: theme2, frontmatter } = useData();
const headers = shallowRef([]);
const hasLocalNav = computed(() => {
return headers.value.length > 0;
});
onContentUpdated(() => {
headers.value = getHeaders(frontmatter.value.outline ?? theme2.value.outline);
});
return {
headers,
hasLocalNav
};
}
// node_modules/vitepress/dist/client/theme-default/without-fonts.js
var theme = {
Layout,
enhanceApp: ({ app }) => {
app.component("Badge", VPBadge);
}
};
var without_fonts_default = theme;
export {
default2 as VPBadge,
default3 as VPButton,
default4 as VPDocAsideSponsors,
default5 as VPFeatures,
default6 as VPHomeContent,
default7 as VPHomeFeatures,
default8 as VPHomeHero,
default9 as VPHomeSponsors,
default10 as VPImage,
default11 as VPLink,
default12 as VPNavBarSearch,
default13 as VPSocialLink,
default14 as VPSocialLinks,
default15 as VPSponsors,
default16 as VPTeamMembers,
default17 as VPTeamPage,
default18 as VPTeamPageSection,
default19 as VPTeamPageTitle,
without_fonts_default as default,
useLocalNav,
useSidebar
};
//# sourceMappingURL=@theme_index.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,40 @@
{
"hash": "33e82b21",
"configHash": "c6db372a",
"lockfileHash": "e3b0c442",
"browserHash": "66861689",
"optimized": {
"vue": {
"src": "../../../../../node_modules/vue/dist/vue.runtime.esm-bundler.js",
"file": "vue.js",
"fileHash": "415ad31e",
"needsInterop": false
},
"vitepress > @vue/devtools-api": {
"src": "../../../../../node_modules/@vue/devtools-api/dist/index.js",
"file": "vitepress___@vue_devtools-api.js",
"fileHash": "82c7da90",
"needsInterop": false
},
"vitepress > @vueuse/core": {
"src": "../../../../../node_modules/@vueuse/core/index.mjs",
"file": "vitepress___@vueuse_core.js",
"fileHash": "bf2a3493",
"needsInterop": false
},
"@theme/index": {
"src": "../../../../../node_modules/vitepress/dist/client/theme-default/index.js",
"file": "@theme_index.js",
"fileHash": "0d87b191",
"needsInterop": false
}
},
"chunks": {
"chunk-RLEUDPPB": {
"file": "chunk-RLEUDPPB.js"
},
"chunk-3S55Y3P7": {
"file": "chunk-3S55Y3P7.js"
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
{
"type": "module"
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,583 @@
import {
DefaultMagicKeysAliasMap,
StorageSerializers,
TransitionPresets,
assert,
breakpointsAntDesign,
breakpointsBootstrapV5,
breakpointsElement,
breakpointsMasterCss,
breakpointsPrimeFlex,
breakpointsQuasar,
breakpointsSematic,
breakpointsTailwind,
breakpointsVuetify,
breakpointsVuetifyV2,
breakpointsVuetifyV3,
bypassFilter,
camelize,
clamp,
cloneFnJSON,
computedAsync,
computedEager,
computedInject,
computedWithControl,
containsProp,
controlledRef,
createEventHook,
createFetch,
createFilterWrapper,
createGlobalState,
createInjectionState,
createRef,
createReusableTemplate,
createSharedComposable,
createSingletonPromise,
createTemplatePromise,
createUnrefFn,
customStorageEventName,
debounceFilter,
defaultDocument,
defaultLocation,
defaultNavigator,
defaultWindow,
executeTransition,
extendRef,
formatDate,
formatTimeAgo,
get,
getLifeCycleTarget,
getSSRHandler,
hasOwn,
hyphenate,
identity,
increaseWithUnit,
injectLocal,
invoke,
isClient,
isDef,
isDefined,
isIOS,
isObject,
isWorker,
makeDestructurable,
mapGamepadToXbox360Controller,
noop,
normalizeDate,
notNullish,
now,
objectEntries,
objectOmit,
objectPick,
onClickOutside,
onElementRemoval,
onKeyDown,
onKeyPressed,
onKeyStroke,
onKeyUp,
onLongPress,
onStartTyping,
pausableFilter,
promiseTimeout,
provideLocal,
provideSSRWidth,
pxValue,
rand,
reactify,
reactifyObject,
reactiveComputed,
reactiveOmit,
reactivePick,
refAutoReset,
refDebounced,
refDefault,
refThrottled,
refWithControl,
resolveRef,
resolveUnref,
set,
setSSRHandler,
syncRef,
syncRefs,
templateRef,
throttleFilter,
timestamp,
toArray,
toReactive,
toRef,
toRefs,
toValue,
tryOnBeforeMount,
tryOnBeforeUnmount,
tryOnMounted,
tryOnScopeDispose,
tryOnUnmounted,
unrefElement,
until,
useActiveElement,
useAnimate,
useArrayDifference,
useArrayEvery,
useArrayFilter,
useArrayFind,
useArrayFindIndex,
useArrayFindLast,
useArrayIncludes,
useArrayJoin,
useArrayMap,
useArrayReduce,
useArraySome,
useArrayUnique,
useAsyncQueue,
useAsyncState,
useBase64,
useBattery,
useBluetooth,
useBreakpoints,
useBroadcastChannel,
useBrowserLocation,
useCached,
useClipboard,
useClipboardItems,
useCloned,
useColorMode,
useConfirmDialog,
useCountdown,
useCounter,
useCssVar,
useCurrentElement,
useCycleList,
useDark,
useDateFormat,
useDebounceFn,
useDebouncedRefHistory,
useDeviceMotion,
useDeviceOrientation,
useDevicePixelRatio,
useDevicesList,
useDisplayMedia,
useDocumentVisibility,
useDraggable,
useDropZone,
useElementBounding,
useElementByPoint,
useElementHover,
useElementSize,
useElementVisibility,
useEventBus,
useEventListener,
useEventSource,
useEyeDropper,
useFavicon,
useFetch,
useFileDialog,
useFileSystemAccess,
useFocus,
useFocusWithin,
useFps,
useFullscreen,
useGamepad,
useGeolocation,
useIdle,
useImage,
useInfiniteScroll,
useIntersectionObserver,
useInterval,
useIntervalFn,
useKeyModifier,
useLastChanged,
useLocalStorage,
useMagicKeys,
useManualRefHistory,
useMediaControls,
useMediaQuery,
useMemoize,
useMemory,
useMounted,
useMouse,
useMouseInElement,
useMousePressed,
useMutationObserver,
useNavigatorLanguage,
useNetwork,
useNow,
useObjectUrl,
useOffsetPagination,
useOnline,
usePageLeave,
useParallax,
useParentElement,
usePerformanceObserver,
usePermission,
usePointer,
usePointerLock,
usePointerSwipe,
usePreferredColorScheme,
usePreferredContrast,
usePreferredDark,
usePreferredLanguages,
usePreferredReducedMotion,
usePreferredReducedTransparency,
usePrevious,
useRafFn,
useRefHistory,
useResizeObserver,
useSSRWidth,
useScreenOrientation,
useScreenSafeArea,
useScriptTag,
useScroll,
useScrollLock,
useSessionStorage,
useShare,
useSorted,
useSpeechRecognition,
useSpeechSynthesis,
useStepper,
useStorage,
useStorageAsync,
useStyleTag,
useSupported,
useSwipe,
useTemplateRefsList,
useTextDirection,
useTextSelection,
useTextareaAutosize,
useThrottleFn,
useThrottledRefHistory,
useTimeAgo,
useTimeout,
useTimeoutFn,
useTimeoutPoll,
useTimestamp,
useTitle,
useToNumber,
useToString,
useToggle,
useTransition,
useUrlSearchParams,
useUserMedia,
useVModel,
useVModels,
useVibrate,
useVirtualList,
useWakeLock,
useWebNotification,
useWebSocket,
useWebWorker,
useWebWorkerFn,
useWindowFocus,
useWindowScroll,
useWindowSize,
watchArray,
watchAtMost,
watchDebounced,
watchDeep,
watchIgnorable,
watchImmediate,
watchOnce,
watchPausable,
watchThrottled,
watchTriggerable,
watchWithFilter,
whenever
} from "./chunk-RLEUDPPB.js";
import "./chunk-3S55Y3P7.js";
export {
DefaultMagicKeysAliasMap,
StorageSerializers,
TransitionPresets,
assert,
computedAsync as asyncComputed,
refAutoReset as autoResetRef,
breakpointsAntDesign,
breakpointsBootstrapV5,
breakpointsElement,
breakpointsMasterCss,
breakpointsPrimeFlex,
breakpointsQuasar,
breakpointsSematic,
breakpointsTailwind,
breakpointsVuetify,
breakpointsVuetifyV2,
breakpointsVuetifyV3,
bypassFilter,
camelize,
clamp,
cloneFnJSON,
computedAsync,
computedEager,
computedInject,
computedWithControl,
containsProp,
computedWithControl as controlledComputed,
controlledRef,
createEventHook,
createFetch,
createFilterWrapper,
createGlobalState,
createInjectionState,
reactify as createReactiveFn,
createRef,
createReusableTemplate,
createSharedComposable,
createSingletonPromise,
createTemplatePromise,
createUnrefFn,
customStorageEventName,
debounceFilter,
refDebounced as debouncedRef,
watchDebounced as debouncedWatch,
defaultDocument,
defaultLocation,
defaultNavigator,
defaultWindow,
computedEager as eagerComputed,
executeTransition,
extendRef,
formatDate,
formatTimeAgo,
get,
getLifeCycleTarget,
getSSRHandler,
hasOwn,
hyphenate,
identity,
watchIgnorable as ignorableWatch,
increaseWithUnit,
injectLocal,
invoke,
isClient,
isDef,
isDefined,
isIOS,
isObject,
isWorker,
makeDestructurable,
mapGamepadToXbox360Controller,
noop,
normalizeDate,
notNullish,
now,
objectEntries,
objectOmit,
objectPick,
onClickOutside,
onElementRemoval,
onKeyDown,
onKeyPressed,
onKeyStroke,
onKeyUp,
onLongPress,
onStartTyping,
pausableFilter,
watchPausable as pausableWatch,
promiseTimeout,
provideLocal,
provideSSRWidth,
pxValue,
rand,
reactify,
reactifyObject,
reactiveComputed,
reactiveOmit,
reactivePick,
refAutoReset,
refDebounced,
refDefault,
refThrottled,
refWithControl,
resolveRef,
resolveUnref,
set,
setSSRHandler,
syncRef,
syncRefs,
templateRef,
throttleFilter,
refThrottled as throttledRef,
watchThrottled as throttledWatch,
timestamp,
toArray,
toReactive,
toRef,
toRefs,
toValue,
tryOnBeforeMount,
tryOnBeforeUnmount,
tryOnMounted,
tryOnScopeDispose,
tryOnUnmounted,
unrefElement,
until,
useActiveElement,
useAnimate,
useArrayDifference,
useArrayEvery,
useArrayFilter,
useArrayFind,
useArrayFindIndex,
useArrayFindLast,
useArrayIncludes,
useArrayJoin,
useArrayMap,
useArrayReduce,
useArraySome,
useArrayUnique,
useAsyncQueue,
useAsyncState,
useBase64,
useBattery,
useBluetooth,
useBreakpoints,
useBroadcastChannel,
useBrowserLocation,
useCached,
useClipboard,
useClipboardItems,
useCloned,
useColorMode,
useConfirmDialog,
useCountdown,
useCounter,
useCssVar,
useCurrentElement,
useCycleList,
useDark,
useDateFormat,
refDebounced as useDebounce,
useDebounceFn,
useDebouncedRefHistory,
useDeviceMotion,
useDeviceOrientation,
useDevicePixelRatio,
useDevicesList,
useDisplayMedia,
useDocumentVisibility,
useDraggable,
useDropZone,
useElementBounding,
useElementByPoint,
useElementHover,
useElementSize,
useElementVisibility,
useEventBus,
useEventListener,
useEventSource,
useEyeDropper,
useFavicon,
useFetch,
useFileDialog,
useFileSystemAccess,
useFocus,
useFocusWithin,
useFps,
useFullscreen,
useGamepad,
useGeolocation,
useIdle,
useImage,
useInfiniteScroll,
useIntersectionObserver,
useInterval,
useIntervalFn,
useKeyModifier,
useLastChanged,
useLocalStorage,
useMagicKeys,
useManualRefHistory,
useMediaControls,
useMediaQuery,
useMemoize,
useMemory,
useMounted,
useMouse,
useMouseInElement,
useMousePressed,
useMutationObserver,
useNavigatorLanguage,
useNetwork,
useNow,
useObjectUrl,
useOffsetPagination,
useOnline,
usePageLeave,
useParallax,
useParentElement,
usePerformanceObserver,
usePermission,
usePointer,
usePointerLock,
usePointerSwipe,
usePreferredColorScheme,
usePreferredContrast,
usePreferredDark,
usePreferredLanguages,
usePreferredReducedMotion,
usePreferredReducedTransparency,
usePrevious,
useRafFn,
useRefHistory,
useResizeObserver,
useSSRWidth,
useScreenOrientation,
useScreenSafeArea,
useScriptTag,
useScroll,
useScrollLock,
useSessionStorage,
useShare,
useSorted,
useSpeechRecognition,
useSpeechSynthesis,
useStepper,
useStorage,
useStorageAsync,
useStyleTag,
useSupported,
useSwipe,
useTemplateRefsList,
useTextDirection,
useTextSelection,
useTextareaAutosize,
refThrottled as useThrottle,
useThrottleFn,
useThrottledRefHistory,
useTimeAgo,
useTimeout,
useTimeoutFn,
useTimeoutPoll,
useTimestamp,
useTitle,
useToNumber,
useToString,
useToggle,
useTransition,
useUrlSearchParams,
useUserMedia,
useVModel,
useVModels,
useVibrate,
useVirtualList,
useWakeLock,
useWebNotification,
useWebSocket,
useWebWorker,
useWebWorkerFn,
useWindowFocus,
useWindowScroll,
useWindowSize,
watchArray,
watchAtMost,
watchDebounced,
watchDeep,
watchIgnorable,
watchImmediate,
watchOnce,
watchPausable,
watchThrottled,
watchTriggerable,
watchWithFilter,
whenever
};
//# sourceMappingURL=vitepress___@vueuse_core.js.map

View File

@@ -0,0 +1,7 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}

View File

@@ -0,0 +1,347 @@
import {
BaseTransition,
BaseTransitionPropsValidators,
Comment,
DeprecationTypes,
EffectScope,
ErrorCodes,
ErrorTypeStrings,
Fragment,
KeepAlive,
ReactiveEffect,
Static,
Suspense,
Teleport,
Text,
TrackOpTypes,
Transition,
TransitionGroup,
TriggerOpTypes,
VueElement,
assertNumber,
callWithAsyncErrorHandling,
callWithErrorHandling,
camelize,
capitalize,
cloneVNode,
compatUtils,
compile,
computed,
createApp,
createBaseVNode,
createBlock,
createCommentVNode,
createElementBlock,
createHydrationRenderer,
createPropsRestProxy,
createRenderer,
createSSRApp,
createSlots,
createStaticVNode,
createTextVNode,
createVNode,
customRef,
defineAsyncComponent,
defineComponent,
defineCustomElement,
defineEmits,
defineExpose,
defineModel,
defineOptions,
defineProps,
defineSSRCustomElement,
defineSlots,
devtools,
effect,
effectScope,
getCurrentInstance,
getCurrentScope,
getCurrentWatcher,
getTransitionRawChildren,
guardReactiveProps,
h,
handleError,
hasInjectionContext,
hydrate,
hydrateOnIdle,
hydrateOnInteraction,
hydrateOnMediaQuery,
hydrateOnVisible,
initCustomFormatter,
initDirectivesForSSR,
inject,
isMemoSame,
isProxy,
isReactive,
isReadonly,
isRef,
isRuntimeOnly,
isShallow,
isVNode,
markRaw,
mergeDefaults,
mergeModels,
mergeProps,
nextTick,
nodeOps,
normalizeClass,
normalizeProps,
normalizeStyle,
onActivated,
onBeforeMount,
onBeforeUnmount,
onBeforeUpdate,
onDeactivated,
onErrorCaptured,
onMounted,
onRenderTracked,
onRenderTriggered,
onScopeDispose,
onServerPrefetch,
onUnmounted,
onUpdated,
onWatcherCleanup,
openBlock,
patchProp,
popScopeId,
provide,
proxyRefs,
pushScopeId,
queuePostFlushCb,
reactive,
readonly,
ref,
registerRuntimeCompiler,
render,
renderList,
renderSlot,
resolveComponent,
resolveDirective,
resolveDynamicComponent,
resolveFilter,
resolveTransitionHooks,
setBlockTracking,
setDevtoolsHook,
setTransitionHooks,
shallowReactive,
shallowReadonly,
shallowRef,
ssrContextKey,
ssrUtils,
stop,
toDisplayString,
toHandlerKey,
toHandlers,
toRaw,
toRef,
toRefs,
toValue,
transformVNodeArgs,
triggerRef,
unref,
useAttrs,
useCssModule,
useCssVars,
useHost,
useId,
useModel,
useSSRContext,
useShadowRoot,
useSlots,
useTemplateRef,
useTransitionState,
vModelCheckbox,
vModelDynamic,
vModelRadio,
vModelSelect,
vModelText,
vShow,
version,
warn,
watch,
watchEffect,
watchPostEffect,
watchSyncEffect,
withAsyncContext,
withCtx,
withDefaults,
withDirectives,
withKeys,
withMemo,
withModifiers,
withScopeId
} from "./chunk-3S55Y3P7.js";
export {
BaseTransition,
BaseTransitionPropsValidators,
Comment,
DeprecationTypes,
EffectScope,
ErrorCodes,
ErrorTypeStrings,
Fragment,
KeepAlive,
ReactiveEffect,
Static,
Suspense,
Teleport,
Text,
TrackOpTypes,
Transition,
TransitionGroup,
TriggerOpTypes,
VueElement,
assertNumber,
callWithAsyncErrorHandling,
callWithErrorHandling,
camelize,
capitalize,
cloneVNode,
compatUtils,
compile,
computed,
createApp,
createBlock,
createCommentVNode,
createElementBlock,
createBaseVNode as createElementVNode,
createHydrationRenderer,
createPropsRestProxy,
createRenderer,
createSSRApp,
createSlots,
createStaticVNode,
createTextVNode,
createVNode,
customRef,
defineAsyncComponent,
defineComponent,
defineCustomElement,
defineEmits,
defineExpose,
defineModel,
defineOptions,
defineProps,
defineSSRCustomElement,
defineSlots,
devtools,
effect,
effectScope,
getCurrentInstance,
getCurrentScope,
getCurrentWatcher,
getTransitionRawChildren,
guardReactiveProps,
h,
handleError,
hasInjectionContext,
hydrate,
hydrateOnIdle,
hydrateOnInteraction,
hydrateOnMediaQuery,
hydrateOnVisible,
initCustomFormatter,
initDirectivesForSSR,
inject,
isMemoSame,
isProxy,
isReactive,
isReadonly,
isRef,
isRuntimeOnly,
isShallow,
isVNode,
markRaw,
mergeDefaults,
mergeModels,
mergeProps,
nextTick,
nodeOps,
normalizeClass,
normalizeProps,
normalizeStyle,
onActivated,
onBeforeMount,
onBeforeUnmount,
onBeforeUpdate,
onDeactivated,
onErrorCaptured,
onMounted,
onRenderTracked,
onRenderTriggered,
onScopeDispose,
onServerPrefetch,
onUnmounted,
onUpdated,
onWatcherCleanup,
openBlock,
patchProp,
popScopeId,
provide,
proxyRefs,
pushScopeId,
queuePostFlushCb,
reactive,
readonly,
ref,
registerRuntimeCompiler,
render,
renderList,
renderSlot,
resolveComponent,
resolveDirective,
resolveDynamicComponent,
resolveFilter,
resolveTransitionHooks,
setBlockTracking,
setDevtoolsHook,
setTransitionHooks,
shallowReactive,
shallowReadonly,
shallowRef,
ssrContextKey,
ssrUtils,
stop,
toDisplayString,
toHandlerKey,
toHandlers,
toRaw,
toRef,
toRefs,
toValue,
transformVNodeArgs,
triggerRef,
unref,
useAttrs,
useCssModule,
useCssVars,
useHost,
useId,
useModel,
useSSRContext,
useShadowRoot,
useSlots,
useTemplateRef,
useTransitionState,
vModelCheckbox,
vModelDynamic,
vModelRadio,
vModelSelect,
vModelText,
vShow,
version,
warn,
watch,
watchEffect,
watchPostEffect,
watchSyncEffect,
withAsyncContext,
withCtx,
withDefaults,
withDirectives,
withKeys,
withMemo,
withModifiers,
withScopeId
};
//# sourceMappingURL=vue.js.map

View File

@@ -0,0 +1,7 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}

View File

@@ -0,0 +1,68 @@
import { defineConfig } from 'vitepress'
const isDev = process.env.NODE_ENV === 'development'
export default defineConfig({
title: "SigPro",
description: "Minimalist Reactive Library",
outDir: '../../docs',
base: isDev ? '/absproxy/5174/sigpro/' : '/sigpro/',
// CONFIGURACIÓN DE VITE (Motor interno)
vite: {
outDir: '../../docs',
base: isDev ? '/absproxy/5174/sigpro/' : '/sigpro/',
server: {
allowedHosts: true,
port: 5174,
}
},
themeConfig: {
logo: '/logo.svg',
nav: [
{ text: 'Home', link: '/' },
{ text: 'Guide', link: '/guide/getting-started' },
{ text: 'Api', link: '/api/quick' },
{ text: 'UI', link: '/ui/intro' },
],
sidebar: [
{
text: 'Introduction',
items: [
{ text: 'What is SigPro?', link: '/' },
{ text: 'Why', link: '/guide/why' },
{ text: 'Guide', link: '/guide/getting-started' },
]
},
{
text: 'API Reference',
items: [
{ text: 'Quick Start', link: '/api/quick' },
{ text: 'Signals', link: '/api/signals' },
{ text: 'Effects', link: '/api/effects' },
{ text: 'Storage', link: '/api/storage' },
{ text: 'Fetch', link: '/api/fetch' },
{ text: 'Pages', link: '/api/pages' },
{ text: 'Components', link: '/api/components' },
{ text: 'Routing', link: '/api/routing' },
]
},
{
text: 'SigPro UI',
items: [
{ text: 'Intro', link: '/ui/intro' },
]
},
{
text: 'Vite Router Plugin',
items: [
{ text: 'Vite Plugin', link: '/vite/plugin' },
]
}
],
socialLinks: [
{ icon: 'github', link: 'https://github.com/natxocc/sigpro' }
]
}
})

View File

@@ -0,0 +1,760 @@
# Components API 🧩
Components in SigPro are native Web Components built on the Custom Elements standard. They provide a way to create reusable, encapsulated pieces of UI with reactive properties and automatic cleanup.
## `$.component(tagName, setupFunction, observedAttributes, useShadowDOM)`
Creates a custom element with reactive properties and automatic dependency tracking.
```javascript
import { $, html } from 'sigpro';
$.component('my-button', (props, { slot, emit }) => {
return html`
<button
class="btn"
@click=${() => emit('click')}
>
${slot()}
</button>
`;
}, ['variant']); // Observe the 'variant' attribute
```
## 📋 API Reference
### Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `tagName` | `string` | required | Custom element tag name (must include a hyphen, e.g., `my-button`) |
| `setupFunction` | `Function` | required | Function that returns the component's template |
| `observedAttributes` | `string[]` | `[]` | Attributes to observe for changes (become reactive props) |
| `useShadowDOM` | `boolean` | `false` | `true` = Shadow DOM (encapsulated), `false` = Light DOM (inherits styles) |
### Setup Function Parameters
The setup function receives two arguments:
1. **`props`** - Object containing reactive signals for each observed attribute
2. **`context`** - Object with helper methods and properties
#### Context Object Properties
| Property | Type | Description |
|----------|------|-------------|
| `slot(name)` | `Function` | Returns array of child nodes for the specified slot |
| `emit(name, detail)` | `Function` | Dispatches a custom event |
| `select(selector)` | `Function` | Query selector within component's root |
| `selectAll(selector)` | `Function` | Query selector all within component's root |
| `host` | `HTMLElement` | Reference to the custom element instance |
| `root` | `Node` | Component's root (shadow root or element itself) |
| `onUnmount(callback)` | `Function` | Register cleanup function |
## 🏠 Light DOM vs Shadow DOM
### Light DOM (`useShadowDOM = false`) - Default
The component **inherits global styles** from the application. Perfect for components that should integrate with your site's design system.
```javascript
// Button that uses global Tailwind CSS
$.component('tw-button', (props, { slot, emit }) => {
const variant = props.variant() || 'primary';
const variants = {
primary: 'bg-blue-500 hover:bg-blue-600 text-white',
secondary: 'bg-gray-500 hover:bg-gray-600 text-white',
outline: 'border border-blue-500 text-blue-500 hover:bg-blue-50'
};
return html`
<button
class="px-4 py-2 rounded font-semibold transition-colors ${variants[variant]}"
@click=${() => emit('click')}
>
${slot()}
</button>
`;
}, ['variant']);
```
### Shadow DOM (`useShadowDOM = true`) - Encapsulated
The component **encapsulates its styles** completely. External styles don't affect it, and its styles don't leak out.
```javascript
// Calendar with encapsulated styles
$.component('ui-calendar', (props) => {
return html`
<style>
/* These styles won't affect the rest of the page */
.calendar {
font-family: system-ui, sans-serif;
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.day {
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 50%;
}
.day.selected {
background: #2196f3;
color: white;
}
</style>
<div class="calendar">
${renderCalendar(props.date())}
</div>
`;
}, ['date'], true); // true = use Shadow DOM
```
## 🎯 Basic Examples
### Simple Counter Component
```javascript
// counter.js
$.component('my-counter', (props) => {
const count = $(0);
return html`
<div class="counter">
<p>Count: ${count}</p>
<button @click=${() => count(c => c + 1)}>+</button>
<button @click=${() => count(c => c - 1)}>-</button>
<button @click=${() => count(0)}>Reset</button>
</div>
`;
});
```
**Usage:**
```html
<my-counter></my-counter>
```
### Component with Props
```javascript
// greeting.js
$.component('my-greeting', (props) => {
const name = props.name() || 'World';
const greeting = $(() => `Hello, ${name}!`);
return html`
<div class="greeting">
<h1>${greeting}</h1>
<p>This is a greeting component.</p>
</div>
`;
}, ['name']); // Observe the 'name' attribute
```
**Usage:**
```html
<my-greeting name="John"></my-greeting>
<my-greeting name="Jane"></my-greeting>
```
### Component with Events
```javascript
// toggle.js
$.component('my-toggle', (props, { emit }) => {
const isOn = $(props.initial() === 'on');
const toggle = () => {
isOn(!isOn());
emit('toggle', { isOn: isOn() });
emit(isOn() ? 'on' : 'off');
};
return html`
<button
class="toggle ${() => isOn() ? 'active' : ''}"
@click=${toggle}
>
${() => isOn() ? 'ON' : 'OFF'}
</button>
`;
}, ['initial']);
```
**Usage:**
```html
<my-toggle
initial="off"
@toggle=${(e) => console.log('Toggled:', e.detail)}
@on=${() => console.log('Turned on')}
@off=${() => console.log('Turned off')}
></my-toggle>
```
## 🎨 Advanced Examples
### Form Input Component
```javascript
// form-input.js
$.component('form-input', (props, { emit }) => {
const value = $(props.value() || '');
const error = $(null);
const touched = $(false);
// Validation effect
$.effect(() => {
if (props.pattern() && touched()) {
const regex = new RegExp(props.pattern());
const isValid = regex.test(value());
error(isValid ? null : props.errorMessage() || 'Invalid input');
emit('validate', { isValid, value: value() });
}
});
const handleInput = (e) => {
value(e.target.value);
emit('update', e.target.value);
};
const handleBlur = () => {
touched(true);
};
return html`
<div class="form-group">
${props.label() ? html`
<label class="form-label">
${props.label()}
${props.required() ? html`<span class="required">*</span>` : ''}
</label>
` : ''}
<input
type="${props.type() || 'text'}"
class="form-control ${() => error() ? 'is-invalid' : ''}"
:value=${value}
@input=${handleInput}
@blur=${handleBlur}
placeholder="${props.placeholder() || ''}"
?disabled=${props.disabled}
?required=${props.required}
/>
${() => error() ? html`
<div class="error-message">${error()}</div>
` : ''}
${props.helpText() ? html`
<small class="help-text">${props.helpText()}</small>
` : ''}
</div>
`;
}, ['label', 'type', 'value', 'placeholder', 'disabled', 'required', 'pattern', 'errorMessage', 'helpText']);
```
**Usage:**
```html
<form-input
label="Email"
type="email"
required
pattern="^[^\s@]+@[^\s@]+\.[^\s@]+$"
errorMessage="Please enter a valid email"
@update=${(e) => formData.email = e.detail}
@validate=${(e) => setEmailValid(e.detail.isValid)}
>
</form-input>
```
### Modal/Dialog Component
```javascript
// modal.js
$.component('my-modal', (props, { slot, emit, onUnmount }) => {
const isOpen = $(false);
// Handle escape key
const handleKeydown = (e) => {
if (e.key === 'Escape' && isOpen()) {
close();
}
};
$.effect(() => {
if (isOpen()) {
document.addEventListener('keydown', handleKeydown);
document.body.style.overflow = 'hidden';
} else {
document.removeEventListener('keydown', handleKeydown);
document.body.style.overflow = '';
}
});
// Cleanup on unmount
onUnmount(() => {
document.removeEventListener('keydown', handleKeydown);
document.body.style.overflow = '';
});
const open = () => {
isOpen(true);
emit('open');
};
const close = () => {
isOpen(false);
emit('close');
};
// Expose methods to parent
props.open = open;
props.close = close;
return html`
<div>
<!-- Trigger button -->
<button
class="modal-trigger"
@click=${open}
>
${slot('trigger') || 'Open Modal'}
</button>
<!-- Modal overlay -->
${() => isOpen() ? html`
<div class="modal-overlay" @click=${close}>
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>${props.title() || 'Modal'}</h3>
<button class="close-btn" @click=${close}>&times;</button>
</div>
<div class="modal-body">
${slot('body')}
</div>
<div class="modal-footer">
${slot('footer') || html`
<button @click=${close}>Close</button>
`}
</div>
</div>
</div>
` : ''}
</div>
`;
}, ['title'], false);
```
**Usage:**
```html
<my-modal title="Confirm Delete">
<button slot="trigger">Delete Item</button>
<div slot="body">
<p>Are you sure you want to delete this item?</p>
<p class="warning">This action cannot be undone.</p>
</div>
<div slot="footer">
<button class="cancel" @click=${close}>Cancel</button>
<button class="delete" @click=${handleDelete}>Delete</button>
</div>
</my-modal>
```
### Data Table Component
```javascript
// data-table.js
$.component('data-table', (props, { emit }) => {
const data = $(props.data() || []);
const columns = $(props.columns() || []);
const sortColumn = $(null);
const sortDirection = $('asc');
const filterText = $('');
// Computed: filtered and sorted data
const processedData = $(() => {
let result = [...data()];
// Filter
if (filterText()) {
const search = filterText().toLowerCase();
result = result.filter(row =>
Object.values(row).some(val =>
String(val).toLowerCase().includes(search)
)
);
}
// Sort
if (sortColumn()) {
const col = sortColumn();
const direction = sortDirection() === 'asc' ? 1 : -1;
result.sort((a, b) => {
if (a[col] < b[col]) return -direction;
if (a[col] > b[col]) return direction;
return 0;
});
}
return result;
});
const handleSort = (col) => {
if (sortColumn() === col) {
sortDirection(sortDirection() === 'asc' ? 'desc' : 'asc');
} else {
sortColumn(col);
sortDirection('asc');
}
emit('sort', { column: col, direction: sortDirection() });
};
return html`
<div class="data-table">
<!-- Search input -->
<div class="table-toolbar">
<input
type="search"
:value=${filterText}
placeholder="Search..."
class="search-input"
/>
<span class="record-count">
${() => `${processedData().length} of ${data().length} records`}
</span>
</div>
<!-- Table -->
<table>
<thead>
<tr>
${columns().map(col => html`
<th
@click=${() => handleSort(col.field)}
class:sortable=${true}
class:sorted=${() => sortColumn() === col.field}
>
${col.label}
${() => sortColumn() === col.field ? html`
<span class="sort-icon">
${sortDirection() === 'asc' ? '↑' : '↓'}
</span>
` : ''}
</th>
`)}
</tr>
</thead>
<tbody>
${() => processedData().map(row => html`
<tr @click=${() => emit('row-click', row)}>
${columns().map(col => html`
<td>${row[col.field]}</td>
`)}
</tr>
`)}
</tbody>
</table>
<!-- Empty state -->
${() => processedData().length === 0 ? html`
<div class="empty-state">
No data found
</div>
` : ''}
</div>
`;
}, ['data', 'columns']);
```
**Usage:**
```javascript
const userColumns = [
{ field: 'id', label: 'ID' },
{ field: 'name', label: 'Name' },
{ field: 'email', label: 'Email' },
{ field: 'role', label: 'Role' }
];
const userData = [
{ id: 1, name: 'John Doe', email: 'john@example.com', role: 'Admin' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'User' }
];
```
```html
<data-table
.data=${userData}
.columns=${userColumns}
@row-click=${(e) => console.log('Row clicked:', e.detail)}
>
</data-table>
```
### Tabs Component
```javascript
// tabs.js
$.component('my-tabs', (props, { slot, emit }) => {
const activeTab = $(props.active() || 0);
// Get all tab headers from slots
const tabs = $(() => {
const headers = slot('tab');
return headers.map((node, index) => ({
index,
title: node.textContent,
content: slot(`panel-${index}`)[0]
}));
});
$.effect(() => {
emit('change', { index: activeTab(), tab: tabs()[activeTab()] });
});
return html`
<div class="tabs">
<div class="tab-headers">
${tabs().map(tab => html`
<button
class="tab-header ${() => activeTab() === tab.index ? 'active' : ''}"
@click=${() => activeTab(tab.index)}
>
${tab.title}
</button>
`)}
</div>
<div class="tab-panels">
${tabs().map(tab => html`
<div
class="tab-panel"
style="display: ${() => activeTab() === tab.index ? 'block' : 'none'}"
>
${tab.content}
</div>
`)}
</div>
</div>
`;
}, ['active']);
```
**Usage:**
```html
<my-tabs @change=${(e) => console.log('Tab changed:', e.detail)}>
<div slot="tab">Profile</div>
<div slot="panel-0">
<h3>Profile Settings</h3>
<form>...</form>
</div>
<div slot="tab">Security</div>
<div slot="panel-1">
<h3>Security Settings</h3>
<form>...</form>
</div>
<div slot="tab">Notifications</div>
<div slot="panel-2">
<h3>Notification Preferences</h3>
<form>...</form>
</div>
</my-tabs>
```
### Component with External Data
```javascript
// user-profile.js
$.component('user-profile', (props, { emit, onUnmount }) => {
const user = $(null);
const loading = $(false);
const error = $(null);
// Fetch user data when userId changes
$.effect(() => {
const userId = props.userId();
if (!userId) return;
loading(true);
error(null);
const controller = new AbortController();
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then(res => res.json())
.then(data => {
user(data);
emit('loaded', data);
})
.catch(err => {
if (err.name !== 'AbortError') {
error(err.message);
emit('error', err);
}
})
.finally(() => loading(false));
// Cleanup: abort fetch if component unmounts or userId changes
onUnmount(() => controller.abort());
});
return html`
<div class="user-profile">
${() => loading() ? html`
<div class="spinner">Loading...</div>
` : error() ? html`
<div class="error">Error: ${error()}</div>
` : user() ? html`
<div class="user-info">
<img src="${user().avatar}" class="avatar" />
<h2>${user().name}</h2>
<p>${user().email}</p>
<p>Member since: ${new Date(user().joined).toLocaleDateString()}</p>
</div>
` : html`
<div class="no-user">No user selected</div>
`}
</div>
`;
}, ['user-id']);
```
## 📦 Component Libraries
### Building a Reusable Component Library
```javascript
// components/index.js
import { $, html } from 'sigpro';
// Button component
export const Button = $.component('ui-button', (props, { slot, emit }) => {
const variant = props.variant() || 'primary';
const size = props.size() || 'md';
const sizes = {
sm: 'px-2 py-1 text-sm',
md: 'px-4 py-2',
lg: 'px-6 py-3 text-lg'
};
const variants = {
primary: 'bg-blue-500 hover:bg-blue-600 text-white',
secondary: 'bg-gray-500 hover:bg-gray-600 text-white',
danger: 'bg-red-500 hover:bg-red-600 text-white'
};
return html`
<button
class="rounded font-semibold transition-colors ${sizes[size]} ${variants[variant]}"
?disabled=${props.disabled}
@click=${() => emit('click')}
>
${slot()}
</button>
`;
}, ['variant', 'size', 'disabled']);
// Card component
export const Card = $.component('ui-card', (props, { slot }) => {
return html`
<div class="card border rounded-lg shadow-sm overflow-hidden">
${props.title() ? html`
<div class="card-header bg-gray-50 px-4 py-3 border-b">
<h3 class="font-semibold">${props.title()}</h3>
</div>
` : ''}
<div class="card-body p-4">
${slot()}
</div>
${props.footer() ? html`
<div class="card-footer bg-gray-50 px-4 py-3 border-t">
${slot('footer')}
</div>
` : ''}
</div>
`;
}, ['title']);
// Badge component
export const Badge = $.component('ui-badge', (props, { slot }) => {
const type = props.type() || 'default';
const types = {
default: 'bg-gray-100 text-gray-800',
success: 'bg-green-100 text-green-800',
warning: 'bg-yellow-100 text-yellow-800',
error: 'bg-red-100 text-red-800',
info: 'bg-blue-100 text-blue-800'
};
return html`
<span class="inline-block px-2 py-1 text-xs font-semibold rounded ${types[type]}">
${slot()}
</span>
`;
}, ['type']);
export { $, html };
```
**Usage:**
```javascript
import { Button, Card, Badge } from './components/index.js';
// Use components anywhere
const app = html`
<div>
<Card title="Welcome">
<p>This is a card component</p>
<div slot="footer">
<Button variant="primary" @click=${handleClick}>
Save Changes
</Button>
<Badge type="success">New</Badge>
</div>
</Card>
</div>
`;
```
## 🎯 Decision Guide: Light DOM vs Shadow DOM
| Use Light DOM (`false`) when... | Use Shadow DOM (`true`) when... |
|--------------------------------|-------------------------------|
| Component is part of your main app | Building a UI library for others |
| Using global CSS (Tailwind, Bootstrap) | Creating embeddable widgets |
| Need to inherit theme variables | Styles must be pixel-perfect everywhere |
| Working with existing design system | Component has complex, specific styles |
| Quick prototyping | Distributing to different projects |
| Form elements that should match site | Need style isolation/encapsulation |
## 📊 Summary
| Feature | Description |
|---------|-------------|
| **Native Web Components** | Built on Custom Elements standard |
| **Reactive Props** | Observed attributes become signals |
| **Two Rendering Modes** | Light DOM (default) or Shadow DOM |
| **Automatic Cleanup** | Effects and listeners cleaned up on disconnect |
| **Event System** | Custom events with `emit()` |
| **Slot Support** | Full slot API for content projection |
| **Zero Dependencies** | Pure vanilla JavaScript |
---
> **Pro Tip:** Start with Light DOM components for app-specific UI, and use Shadow DOM when building components that need to work identically across different projects or websites.

1039
packages/docs/api/effects.md Normal file

File diff suppressed because it is too large Load Diff

998
packages/docs/api/fetch.md Normal file
View File

@@ -0,0 +1,998 @@
# Fetch API 🌐
SigPro provides a simple, lightweight wrapper around the native Fetch API that integrates seamlessly with signals for loading state management. It's designed for common use cases with sensible defaults.
## Core Concepts
### What is `$.fetch`?
A ultra-simple fetch wrapper that:
- **Automatically handles JSON** serialization and parsing
- **Integrates with signals** for loading state
- **Returns `null` on error** (no try/catch needed for basic usage)
- **Works great with effects** for reactive data fetching
## `$.fetch(url, data, [loading])`
Makes a POST request with JSON data and optional loading signal.
```javascript
import { $ } from 'sigpro';
const loading = $(false);
async function loadUser() {
const user = await $.fetch('/api/user', { id: 123 }, loading);
if (user) {
console.log('User loaded:', user);
}
}
```
## 📋 API Reference
### Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `url` | `string` | Endpoint URL |
| `data` | `Object` | Data to send (automatically JSON.stringify'd) |
| `loading` | `Function` (optional) | Signal function to track loading state |
### Returns
| Return | Description |
|--------|-------------|
| `Promise<Object\|null>` | Parsed JSON response or `null` on error |
## 🎯 Basic Examples
### Simple Data Fetching
```javascript
import { $ } from 'sigpro';
const userData = $(null);
async function fetchUser(id) {
const data = await $.fetch('/api/user', { id });
if (data) {
userData(data);
}
}
fetchUser(123);
```
### With Loading State
```javascript
import { $, html } from 'sigpro';
const user = $(null);
const loading = $(false);
async function loadUser(id) {
const data = await $.fetch('/api/user', { id }, loading);
if (data) user(data);
}
// In your template
html`
<div>
${() => loading() ? html`
<div class="spinner">Loading...</div>
` : user() ? html`
<div>
<h2>${user().name}</h2>
<p>Email: ${user().email}</p>
</div>
` : html`
<p>No user found</p>
`}
</div>
`;
```
### In an Effect
```javascript
import { $ } from 'sigpro';
const userId = $(1);
const user = $(null);
const loading = $(false);
$.effect(() => {
const id = userId();
if (id) {
$.fetch(`/api/users/${id}`, null, loading).then(data => {
if (data) user(data);
});
}
});
userId(2); // Automatically fetches new user
```
## 🚀 Advanced Examples
### User Profile with Loading States
```javascript
import { $, html } from 'sigpro';
const Profile = () => {
const userId = $(1);
const user = $(null);
const loading = $(false);
const error = $(null);
const fetchUser = async (id) => {
error(null);
const data = await $.fetch('/api/user', { id }, loading);
if (data) {
user(data);
} else {
error('Failed to load user');
}
};
// Fetch when userId changes
$.effect(() => {
fetchUser(userId());
});
return html`
<div class="profile">
<div class="user-selector">
<button @click=${() => userId(1)}>User 1</button>
<button @click=${() => userId(2)}>User 2</button>
<button @click=${() => userId(3)}>User 3</button>
</div>
${() => {
if (loading()) {
return html`<div class="spinner">Loading profile...</div>`;
}
if (error()) {
return html`<div class="error">${error()}</div>`;
}
if (user()) {
return html`
<div class="user-info">
<h2>${user().name}</h2>
<p>Email: ${user().email}</p>
<p>Role: ${user().role}</p>
<p>Joined: ${new Date(user().joined).toLocaleDateString()}</p>
</div>
`;
}
return html`<p>Select a user</p>`;
}}
</div>
`;
};
```
### Todo List with API
```javascript
import { $, html } from 'sigpro';
const TodoApp = () => {
const todos = $([]);
const loading = $(false);
const newTodo = $('');
const filter = $('all'); // 'all', 'active', 'completed'
// Load todos
const loadTodos = async () => {
const data = await $.fetch('/api/todos', {}, loading);
if (data) todos(data);
};
// Add todo
const addTodo = async () => {
if (!newTodo().trim()) return;
const todo = await $.fetch('/api/todos', {
text: newTodo(),
completed: false
});
if (todo) {
todos([...todos(), todo]);
newTodo('');
}
};
// Toggle todo
const toggleTodo = async (id, completed) => {
const updated = await $.fetch(`/api/todos/${id}`, {
completed: !completed
});
if (updated) {
todos(todos().map(t =>
t.id === id ? updated : t
));
}
};
// Delete todo
const deleteTodo = async (id) => {
const result = await $.fetch(`/api/todos/${id}/delete`, {});
if (result) {
todos(todos().filter(t => t.id !== id));
}
};
// Filtered todos
const filteredTodos = $(() => {
const currentFilter = filter();
if (currentFilter === 'all') return todos();
if (currentFilter === 'active') {
return todos().filter(t => !t.completed);
}
return todos().filter(t => t.completed);
});
// Load on mount
loadTodos();
return html`
<div class="todo-app">
<h1>Todo List</h1>
<div class="add-todo">
<input
type="text"
:value=${newTodo}
@keydown.enter=${addTodo}
placeholder="Add a new todo..."
/>
<button @click=${addTodo}>Add</button>
</div>
<div class="filters">
<button
class:active=${() => filter() === 'all'}
@click=${() => filter('all')}
>
All
</button>
<button
class:active=${() => filter() === 'active'}
@click=${() => filter('active')}
>
Active
</button>
<button
class:active=${() => filter() === 'completed'}
@click=${() => filter('completed')}
>
Completed
</button>
</div>
${() => loading() ? html`
<div class="spinner">Loading todos...</div>
) : html`
<ul class="todo-list">
${filteredTodos().map(todo => html`
<li class="todo-item">
<input
type="checkbox"
:checked=${todo.completed}
@change=${() => toggleTodo(todo.id, todo.completed)}
/>
<span class:completed=${todo.completed}>${todo.text}</span>
<button @click=${() => deleteTodo(todo.id)}>🗑️</button>
</li>
`)}
</ul>
`}
</div>
`;
};
```
### Infinite Scroll with Pagination
```javascript
import { $, html } from 'sigpro';
const InfiniteScroll = () => {
const posts = $([]);
const page = $(1);
const loading = $(false);
const hasMore = $(true);
const error = $(null);
const loadMore = async () => {
if (loading() || !hasMore()) return;
const data = await $.fetch('/api/posts', {
page: page(),
limit: 10
}, loading);
if (data) {
if (data.posts.length === 0) {
hasMore(false);
} else {
posts([...posts(), ...data.posts]);
page(p => p + 1);
}
} else {
error('Failed to load posts');
}
};
// Intersection Observer for infinite scroll
$.effect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
loadMore();
}
},
{ threshold: 0.1 }
);
const sentinel = document.getElementById('sentinel');
if (sentinel) observer.observe(sentinel);
return () => observer.disconnect();
});
// Initial load
loadMore();
return html`
<div class="infinite-scroll">
<h1>Posts</h1>
<div class="posts">
${posts().map(post => html`
<article class="post">
<h2>${post.title}</h2>
<p>${post.body}</p>
<small>By ${post.author}</small>
</article>
`)}
</div>
<div id="sentinel" class="sentinel">
${() => {
if (loading()) {
return html`<div class="spinner">Loading more...</div>`;
}
if (error()) {
return html`<div class="error">${error()}</div>`;
}
if (!hasMore()) {
return html`<div class="end">No more posts</div>`;
}
return '';
}}
</div>
</div>
`;
};
```
### Search with Debounce
```javascript
import { $, html } from 'sigpro';
const SearchComponent = () => {
const query = $('');
const results = $([]);
const loading = $(false);
const error = $(null);
let searchTimeout;
const performSearch = async (searchQuery) => {
if (!searchQuery.trim()) {
results([]);
return;
}
const data = await $.fetch('/api/search', {
q: searchQuery
}, loading);
if (data) {
results(data);
} else {
error('Search failed');
}
};
// Debounced search
$.effect(() => {
const searchQuery = query();
clearTimeout(searchTimeout);
if (searchQuery.length < 2) {
results([]);
return;
}
searchTimeout = setTimeout(() => {
performSearch(searchQuery);
}, 300);
return () => clearTimeout(searchTimeout);
});
return html`
<div class="search">
<div class="search-box">
<input
type="search"
:value=${query}
placeholder="Search..."
class="search-input"
/>
${() => loading() ? html`
<span class="spinner-small">⌛</span>
) : ''}
</div>
${() => {
if (error()) {
return html`<div class="error">${error()}</div>`;
}
if (results().length > 0) {
return html`
<ul class="results">
${results().map(item => html`
<li class="result-item">
<h3>${item.title}</h3>
<p>${item.description}</p>
</li>
`)}
</ul>
`;
}
if (query().length >= 2 && !loading()) {
return html`<p class="no-results">No results found</p>`;
}
return '';
}}
</div>
`;
};
```
### Form Submission
```javascript
import { $, html } from 'sigpro';
const ContactForm = () => {
const formData = $({
name: '',
email: '',
message: ''
});
const submitting = $(false);
const submitError = $(null);
const submitSuccess = $(false);
const handleSubmit = async (e) => {
e.preventDefault();
submitError(null);
submitSuccess(false);
const result = await $.fetch('/api/contact', formData(), submitting);
if (result) {
submitSuccess(true);
formData({ name: '', email: '', message: '' });
} else {
submitError('Failed to send message. Please try again.');
}
};
const updateField = (field, value) => {
formData({
...formData(),
[field]: value
});
};
return html`
<form class="contact-form" @submit=${handleSubmit}>
<h2>Contact Us</h2>
<div class="form-group">
<label for="name">Name:</label>
<input
type="text"
id="name"
:value=${() => formData().name}
@input=${(e) => updateField('name', e.target.value)}
required
?disabled=${submitting}
/>
</div>
<div class="form-group">
<label for="email">Email:</label>
<input
type="email"
id="email"
:value=${() => formData().email}
@input=${(e) => updateField('email', e.target.value)}
required
?disabled=${submitting}
/>
</div>
<div class="form-group">
<label for="message">Message:</label>
<textarea
id="message"
:value=${() => formData().message}
@input=${(e) => updateField('message', e.target.value)}
required
rows="5"
?disabled=${submitting}
></textarea>
</div>
${() => {
if (submitting()) {
return html`<div class="submitting">Sending...</div>`;
}
if (submitError()) {
return html`<div class="error">${submitError()}</div>`;
}
if (submitSuccess()) {
return html`<div class="success">Message sent successfully!</div>`;
}
return '';
}}
<button
type="submit"
?disabled=${submitting}
>
Send Message
</button>
</form>
`;
};
```
### Real-time Dashboard with Multiple Endpoints
```javascript
import { $, html } from 'sigpro';
const Dashboard = () => {
// Multiple data streams
const metrics = $({});
const alerts = $([]);
const logs = $([]);
const loading = $({
metrics: false,
alerts: false,
logs: false
});
const refreshInterval = $(5000); // 5 seconds
const fetchMetrics = async () => {
const data = await $.fetch('/api/metrics', {}, loading().metrics);
if (data) metrics(data);
};
const fetchAlerts = async () => {
const data = await $.fetch('/api/alerts', {}, loading().alerts);
if (data) alerts(data);
};
const fetchLogs = async () => {
const data = await $.fetch('/api/logs', {
limit: 50
}, loading().logs);
if (data) logs(data);
};
// Auto-refresh all data
$.effect(() => {
fetchMetrics();
fetchAlerts();
fetchLogs();
const interval = setInterval(() => {
fetchMetrics();
fetchAlerts();
}, refreshInterval());
return () => clearInterval(interval);
});
return html`
<div class="dashboard">
<header>
<h1>System Dashboard</h1>
<div class="refresh-control">
<label>
Refresh interval:
<select :value=${refreshInterval} @change=${(e) => refreshInterval(parseInt(e.target.value))}>
<option value="2000">2 seconds</option>
<option value="5000">5 seconds</option>
<option value="10000">10 seconds</option>
<option value="30000">30 seconds</option>
</select>
</label>
</div>
</header>
<div class="dashboard-grid">
<!-- Metrics Panel -->
<div class="panel metrics">
<h2>System Metrics</h2>
${() => loading().metrics ? html`
<div class="spinner">Loading metrics...</div>
) : html`
<div class="metrics-grid">
<div class="metric">
<label>CPU</label>
<span>${metrics().cpu || 0}%</span>
</div>
<div class="metric">
<label>Memory</label>
<span>${metrics().memory || 0}%</span>
</div>
<div class="metric">
<label>Requests</label>
<span>${metrics().requests || 0}/s</span>
</div>
</div>
`}
</div>
<!-- Alerts Panel -->
<div class="panel alerts">
<h2>Active Alerts</h2>
${() => loading().alerts ? html`
<div class="spinner">Loading alerts...</div>
) : alerts().length > 0 ? html`
<ul>
${alerts().map(alert => html`
<li class="alert ${alert.severity}">
<strong>${alert.type}</strong>
<p>${alert.message}</p>
<small>${new Date(alert.timestamp).toLocaleTimeString()}</small>
</li>
`)}
</ul>
) : html`
<p class="no-data">No active alerts</p>
`}
</div>
<!-- Logs Panel -->
<div class="panel logs">
<h2>Recent Logs</h2>
${() => loading().logs ? html`
<div class="spinner">Loading logs...</div>
) : html`
<ul>
${logs().map(log => html`
<li class="log ${log.level}">
<span class="timestamp">${new Date(log.timestamp).toLocaleTimeString()}</span>
<span class="message">${log.message}</span>
</li>
`)}
</ul>
`}
</div>
</div>
</div>
`;
};
```
### File Upload
```javascript
import { $, html } from 'sigpro';
const FileUploader = () => {
const files = $([]);
const uploading = $(false);
const uploadProgress = $({});
const uploadResults = $([]);
const handleFileSelect = (e) => {
files([...e.target.files]);
};
const uploadFiles = async () => {
if (files().length === 0) return;
uploading(true);
uploadResults([]);
for (const file of files()) {
const formData = new FormData();
formData.append('file', file);
// Track progress for this file
uploadProgress({
...uploadProgress(),
[file.name]: 0
});
try {
// Custom fetch for FormData
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
uploadResults([
...uploadResults(),
{ file: file.name, success: true, result }
]);
} catch (error) {
uploadResults([
...uploadResults(),
{ file: file.name, success: false, error: error.message }
]);
}
uploadProgress({
...uploadProgress(),
[file.name]: 100
});
}
uploading(false);
};
return html`
<div class="file-uploader">
<h2>Upload Files</h2>
<input
type="file"
multiple
@change=${handleFileSelect}
?disabled=${uploading}
/>
${() => files().length > 0 ? html`
<div class="file-list">
<h3>Selected Files:</h3>
<ul>
${files().map(file => html`
<li>
${file.name} (${(file.size / 1024).toFixed(2)} KB)
${() => uploadProgress()[file.name] ? html`
<progress value="${uploadProgress()[file.name]}" max="100"></progress>
) : ''}
</li>
`)}
</ul>
<button
@click=${uploadFiles}
?disabled=${uploading}
>
${() => uploading() ? 'Uploading...' : 'Upload Files'}
</button>
</div>
` : ''}
${() => uploadResults().length > 0 ? html`
<div class="upload-results">
<h3>Upload Results:</h3>
<ul>
${uploadResults().map(result => html`
<li class="${result.success ? 'success' : 'error'}">
${result.file}:
${result.success ? 'Uploaded successfully' : `Failed: ${result.error}`}
</li>
`)}
</ul>
</div>
` : ''}
</div>
`;
};
```
### Retry Logic
```javascript
import { $ } from 'sigpro';
// Enhanced fetch with retry
const fetchWithRetry = async (url, data, loading, maxRetries = 3) => {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
if (loading) loading(true);
const result = await $.fetch(url, data);
if (result !== null) {
return result;
}
// If we get null but no error, wait and retry
if (attempt < maxRetries) {
await new Promise(resolve =>
setTimeout(resolve, Math.pow(2, attempt) * 1000) // Exponential backoff
);
}
} catch (error) {
lastError = error;
console.warn(`Attempt ${attempt} failed:`, error);
if (attempt < maxRetries) {
await new Promise(resolve =>
setTimeout(resolve, Math.pow(2, attempt) * 1000)
);
}
} finally {
if (attempt === maxRetries && loading) {
loading(false);
}
}
}
console.error('All retry attempts failed:', lastError);
return null;
};
// Usage
const loading = $(false);
const data = await fetchWithRetry('/api/unreliable-endpoint', {}, loading, 5);
```
## 🎯 Best Practices
### 1. Always Handle Null Responses
```javascript
// ❌ Don't assume success
const data = await $.fetch('/api/data');
console.log(data.property); // Might throw if data is null
// ✅ Check for null
const data = await $.fetch('/api/data');
if (data) {
console.log(data.property);
} else {
showError('Failed to load data');
}
```
### 2. Use with Effects for Reactivity
```javascript
// ❌ Manual fetching
button.addEventListener('click', async () => {
const data = await $.fetch('/api/data');
updateUI(data);
});
// ✅ Reactive fetching
const trigger = $(false);
$.effect(() => {
if (trigger()) {
$.fetch('/api/data').then(data => {
if (data) updateUI(data);
});
}
});
trigger(true); // Triggers fetch
```
### 3. Combine with Loading Signals
```javascript
// ✅ Always show loading state
const loading = $(false);
const data = $(null);
async function load() {
const result = await $.fetch('/api/data', {}, loading);
if (result) data(result);
}
// In template
html`
<div>
${() => loading() ? '<Spinner />' :
data() ? '<Data />' :
'<Empty />'}
</div>
`;
```
### 4. Cancel In-flight Requests
```javascript
// ✅ Use AbortController with effects
let controller;
$.effect(() => {
if (controller) {
controller.abort();
}
controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(res => res.json())
.then(data => {
if (!controller.signal.aborted) {
updateData(data);
}
});
return () => controller.abort();
});
```
## 📊 Error Handling
### Basic Error Handling
```javascript
const data = await $.fetch('/api/data');
if (!data) {
// Handle error (show message, retry, etc.)
}
```
### With Error Signal
```javascript
const data = $(null);
const error = $(null);
const loading = $(false);
async function loadData() {
error(null);
const result = await $.fetch('/api/data', {}, loading);
if (result) {
data(result);
} else {
error('Failed to load data');
}
}
```
---
> **Pro Tip:** Combine `$.fetch` with `$.effect` and loading signals for a complete reactive data fetching solution. The loading signal integration makes it trivial to show loading states in your UI.

497
packages/docs/api/pages.md Normal file
View File

@@ -0,0 +1,497 @@
# Pages API 📄
Pages in SigPro are special components designed for route-based navigation with **automatic cleanup**. When you navigate away from a page, all signals, effects, and event listeners created within that page are automatically cleaned up - no memory leaks, no manual cleanup needed.
## `$.page(setupFunction)`
Creates a page with automatic cleanup of all signals and effects when navigated away.
```javascript
import { $, html } from 'sigpro';
export default $.page(() => {
// All signals and effects created here
// will be automatically cleaned up on navigation
const count = $(0);
$.effect(() => {
console.log(`Count: ${count()}`);
});
return html`
<div>
<h1>My Page</h1>
<p>Count: ${count}</p>
<button @click=${() => count(c => c + 1)}>+</button>
</div>
`;
});
```
## 📋 API Reference
| Parameter | Type | Description |
|-----------|------|-------------|
| `setupFunction` | `Function` | Function that returns the page content. Receives context object with `params` and `onUnmount` |
### Context Object Properties
| Property | Type | Description |
|----------|------|-------------|
| `params` | `Object` | Route parameters passed to the page |
| `onUnmount` | `Function` | Register cleanup callbacks (alternative to automatic cleanup) |
## 🎯 Basic Usage
### Simple Page
```javascript
// pages/home.js
import { $, html } from 'sigpro';
export default $.page(() => {
const title = $('Welcome to SigPro');
return html`
<div class="home-page">
<h1>${title}</h1>
<p>This page will clean itself up when you navigate away.</p>
</div>
`;
});
```
### Page with Route Parameters
```javascript
// pages/user.js
import { $, html } from 'sigpro';
export default $.page(({ params }) => {
// Access route parameters
const userId = params.id;
const userData = $(null);
const loading = $(false);
// Auto-cleaned effect
$.effect(() => {
loading(true);
$.fetch(`/api/users/${userId}`, null, loading)
.then(data => userData(data));
});
return html`
<div>
${() => loading() ? html`
<div class="spinner">Loading...</div>
` : html`
<h1>User Profile: ${userData()?.name}</h1>
<p>Email: ${userData()?.email}</p>
`}
</div>
`;
});
```
## 🧹 Automatic Cleanup
The magic of `$.page` is automatic cleanup. Everything created inside the page is tracked and cleaned up:
```javascript
export default $.page(() => {
// ✅ Signals are auto-cleaned
const count = $(0);
const user = $(null);
// ✅ Effects are auto-cleaned
$.effect(() => {
document.title = `Count: ${count()}`;
});
// ✅ Event listeners are auto-cleaned
window.addEventListener('resize', handleResize);
// ✅ Intervals and timeouts are auto-cleaned
const interval = setInterval(() => {
refreshData();
}, 5000);
return html`<div>Page content</div>`;
});
// When navigating away: all signals, effects, listeners, intervals STOP
```
## 📝 Manual Cleanup with `onUnmount`
Sometimes you need custom cleanup logic. Use `onUnmount` for that:
```javascript
export default $.page(({ onUnmount }) => {
// WebSocket connection
const socket = new WebSocket('wss://api.example.com');
socket.onmessage = (event) => {
updateData(JSON.parse(event.data));
};
// Manual cleanup
onUnmount(() => {
socket.close();
console.log('WebSocket closed');
});
return html`<div>Real-time updates</div>`;
});
```
## 🔄 Integration with Router
Pages are designed to work seamlessly with `$.router`:
```javascript
import { $, html } from 'sigpro';
import HomePage from './pages/Home.js';
import UserPage from './pages/User.js';
import SettingsPage from './pages/Settings.js';
const routes = [
{ path: '/', component: HomePage },
{ path: '/user/:id', component: UserPage },
{ path: '/settings', component: SettingsPage },
];
// Mount router
document.body.appendChild($.router(routes));
```
## 💡 Practical Examples
### Example 1: Data Fetching Page
```javascript
// pages/posts.js
export default $.page(({ params }) => {
const posts = $([]);
const loading = $(true);
const error = $(null);
$.effect(() => {
fetch('/api/posts')
.then(res => res.json())
.then(data => {
posts(data);
loading(false);
})
.catch(err => {
error(err.message);
loading(false);
});
});
return html`
<div class="posts-page">
<h1>Blog Posts</h1>
${() => loading() ? html`
<div class="loading">Loading posts...</div>
` : error() ? html`
<div class="error">Error: ${error()}</div>
` : html`
<div class="posts-grid">
${posts().map(post => html`
<article class="post-card">
<h2>${post.title}</h2>
<p>${post.excerpt}</p>
<a href="#/post/${post.id}">Read more</a>
</article>
`)}
</div>
`}
</div>
`;
});
```
### Example 2: Real-time Dashboard
```javascript
// pages/dashboard.js
export default $.page(({ onUnmount }) => {
const metrics = $({
cpu: 0,
memory: 0,
requests: 0
});
// Auto-refresh data
const refreshInterval = setInterval(async () => {
const data = await $.fetch('/api/metrics');
if (data) metrics(data);
}, 5000);
// Manual cleanup for interval
onUnmount(() => clearInterval(refreshInterval));
// Live clock
const currentTime = $(new Date());
const clockInterval = setInterval(() => {
currentTime(new Date());
}, 1000);
onUnmount(() => clearInterval(clockInterval));
return html`
<div class="dashboard">
<h1>System Dashboard</h1>
<div class="time">
Last updated: ${() => currentTime().toLocaleTimeString()}
</div>
<div class="metrics-grid">
<div class="metric-card">
<h3>CPU Usage</h3>
<p class="metric-value">${() => metrics().cpu}%</p>
</div>
<div class="metric-card">
<h3>Memory Usage</h3>
<p class="metric-value">${() => metrics().memory}%</p>
</div>
<div class="metric-card">
<h3>Requests/min</h3>
<p class="metric-value">${() => metrics().requests}</p>
</div>
</div>
</div>
`;
});
```
### Example 3: Multi-step Form
```javascript
// pages/checkout.js
export default $.page(({ onUnmount }) => {
const step = $(1);
const formData = $({
email: '',
address: '',
payment: ''
});
// Warn user before leaving
const handleBeforeUnload = (e) => {
if (step() < 3) {
e.preventDefault();
e.returnValue = '';
}
};
window.addEventListener('beforeunload', handleBeforeUnload);
onUnmount(() => {
window.removeEventListener('beforeunload', handleBeforeUnload);
});
const nextStep = () => step(s => Math.min(s + 1, 3));
const prevStep = () => step(s => Math.max(s - 1, 1));
return html`
<div class="checkout">
<h1>Checkout - Step ${step} of 3</h1>
${() => {
switch(step()) {
case 1:
return html`
<div class="step">
<h2>Email</h2>
<input
type="email"
:value=${() => formData().email}
@input=${(e) => formData({...formData(), email: e.target.value})}
/>
</div>
`;
case 2:
return html`
<div class="step">
<h2>Address</h2>
<textarea
:value=${() => formData().address}
@input=${(e) => formData({...formData(), address: e.target.value})}
></textarea>
</div>
`;
case 3:
return html`
<div class="step">
<h2>Payment</h2>
<input
type="text"
placeholder="Card number"
:value=${() => formData().payment}
@input=${(e) => formData({...formData(), payment: e.target.value})}
/>
</div>
`;
}
}}
<div class="buttons">
${() => step() > 1 ? html`
<button @click=${prevStep}>Previous</button>
` : ''}
${() => step() < 3 ? html`
<button @click=${nextStep}>Next</button>
` : html`
<button @click=${submitOrder}>Place Order</button>
`}
</div>
</div>
`;
});
```
### Example 4: Page with Tabs
```javascript
// pages/profile.js
export default $.page(({ params }) => {
const activeTab = $('overview');
const userData = $(null);
// Load user data
$.effect(() => {
$.fetch(`/api/users/${params.id}`)
.then(data => userData(data));
});
const tabs = {
overview: () => html`
<div>
<h3>Overview</h3>
<p>Username: ${userData()?.username}</p>
<p>Member since: ${userData()?.joined}</p>
</div>
`,
posts: () => html`
<div>
<h3>Posts</h3>
${userData()?.posts.map(post => html`
<div class="post">${post.title}</div>
`)}
</div>
`,
settings: () => html`
<div>
<h3>Settings</h3>
<label>
<input type="checkbox" :checked=${userData()?.emailNotifications} />
Email notifications
</label>
</div>
`
};
return html`
<div class="profile-page">
<h1>${() => userData()?.name}</h1>
<div class="tabs">
${Object.keys(tabs).map(tab => html`
<button
class:active=${() => activeTab() === tab}
@click=${() => activeTab(tab)}
>
${tab.charAt(0).toUpperCase() + tab.slice(1)}
</button>
`)}
</div>
<div class="tab-content">
${() => tabs[activeTab()]()}
</div>
</div>
`;
});
```
## 🎯 Advanced Patterns
### Page with Nested Routes
```javascript
// pages/settings/index.js
export default $.page(({ params }) => {
const section = params.section || 'general';
const sections = {
general: () => import('./general.js').then(m => m.default),
security: () => import('./security.js').then(m => m.default),
notifications: () => import('./notifications.js').then(m => m.default)
};
const currentSection = $(null);
$.effect(() => {
sections[section]().then(comp => currentSection(comp));
});
return html`
<div class="settings">
<nav>
<a href="#/settings/general">General</a>
<a href="#/settings/security">Security</a>
<a href="#/settings/notifications">Notifications</a>
</nav>
<div class="content">
${currentSection}
</div>
</div>
`;
});
```
### Page with Authentication
```javascript
// pages/dashboard.js
export default $.page(({ onUnmount }) => {
const isAuthenticated = $(false);
const authCheck = $.effect(() => {
const token = localStorage.getItem('token');
isAuthenticated(!!token);
});
// Redirect if not authenticated
$.effect(() => {
if (!isAuthenticated()) {
$.router.go('/login');
}
});
return html`
<div class="dashboard">
<h1>Protected Dashboard</h1>
<!-- Protected content -->
</div>
`;
});
```
## 📊 Summary
| Feature | Description |
|---------|-------------|
| **Automatic Cleanup** | All signals, effects, and resources auto-cleaned on navigation |
| **Memory Safe** | No memory leaks, even with complex nested effects |
| **Router Integration** | Designed to work perfectly with `$.router` |
| **Parameters** | Access route parameters via `params` object |
| **Manual Cleanup** | `onUnmount` for custom cleanup needs |
| **Zero Configuration** | Just wrap your page in `$.page()` and it works |
---
> **Pro Tip:** Always wrap route-based views in `$.page()` to ensure proper cleanup. This prevents memory leaks and ensures your app stays performant even after many navigation changes.

436
packages/docs/api/quick.md Normal file
View File

@@ -0,0 +1,436 @@
# Quick API Reference ⚡
A comprehensive reference for all SigPro APIs. Everything you need to build reactive web applications with signals and web components.
## 📋 API Functions Reference
| Function | Description | Example |
|----------|-------------|---------|
| **`$(initialValue)`** | Creates a reactive signal (getter/setter) | `const count = $(0)` |
| **`$(computedFn)`** | Creates a computed signal | `const full = $(() => first() + last())` |
| **`$.effect(fn)`** | Runs effect when dependencies change | `$.effect(() => console.log(count()))` |
| **`$.page(setupFn)`** | Creates a page with automatic cleanup | `$.page(() => html`<div>Page</div>`)` |
| **`$.component(tagName, setupFn, attrs, useShadow)`** | Creates reactive Web Component | `$.component('my-menu', setup, ['items'])` |
| **`$.router(routes)`** | Creates a hash-based router | `$.router([{path:'/', component:Home}])` |
| **`$.router.go(path)`** | Navigates to a route | `$.router.go('/user/42')` |
| **`$.fetch(url, data, loadingSignal)`** | Fetch wrapper with loading state | `const data = await $.fetch('/api', data, loading)` |
| **`$.storage(key, initialValue, storageType)`** | Persistent signal (local/sessionStorage) | `const theme = $.storage('theme', 'light')` |
| **`` html`...` ``** | Template literal for reactive HTML | `` html`<div>${count}</div>` `` |
### Signal Methods
| Method | Description | Example |
|--------|-------------|---------|
| **`signal()`** | Gets current value | `count()` |
| **`signal(newValue)`** | Sets new value | `count(5)` |
| **`signal(prev => new)`** | Updates using previous value | `count(c => c + 1)` |
### Component Context Properties
| Property | Description | Example |
|----------|-------------|---------|
| **`props`** | Reactive component properties | `props.title()` |
| **`slot(name)`** | Accesses slot content | `slot()` or `slot('footer')` |
| **`emit(event, data)`** | Dispatches custom event | `emit('update', value)` |
| **`onUnmount(cb)`** | Registers cleanup callback | `onUnmount(() => clearInterval(timer))` |
### Page Context Properties
| Property | Description | Example |
|----------|-------------|---------|
| **`params`** | Route parameters | `params.id`, `params.slug` |
| **`onUnmount(cb)`** | Registers cleanup callback | `onUnmount(() => clearInterval(timer))` |
### HTML Directives
| Directive | Description | Example |
|-----------|-------------|---------|
| **`@event`** | Event listener | `` @click=${handler} `` |
| **`:property`** | Two-way binding | `` :value=${signal} `` |
| **`?attribute`** | Boolean attribute | `` ?disabled=${signal} `` |
| **`.property`** | DOM property binding | `` .scrollTop=${value} `` |
| **`class:name`** | Conditional class | `` class:active=${isActive} `` |
<style>
table {
width: 100%;
border-collapse: collapse;
margin: 1.5rem 0;
}
th {
background-color: var(--vp-c-bg-soft);
padding: 0.75rem;
text-align: left;
font-weight: 600;
}
td {
padding: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
tr:hover {
background-color: var(--vp-c-bg-soft);
}
code {
font-size: 0.9em;
padding: 0.2em 0.4em;
border-radius: 4px;
background-color: var(--vp-c-bg-mute);
}
</style>
## 📡 Signals - `$(initialValue)`
Creates a reactive value that notifies dependents when changed.
| Pattern | Example | Description |
|---------|---------|-------------|
| **Basic Signal** | `const count = $(0)` | Create signal with initial value |
| **Getter** | `count()` | Read current value |
| **Setter** | `count(5)` | Set new value directly |
| **Updater** | `count(prev => prev + 1)` | Update based on previous value |
| **Computed** | `const full = $(() => first() + last())` | Auto-updating derived signal |
### Examples
```javascript
// Basic signal
const count = $(0);
console.log(count()); // 0
count(5);
count(c => c + 1); // 6
// Computed signal
const firstName = $('John');
const lastName = $('Doe');
const fullName = $(() => `${firstName()} ${lastName()}`);
console.log(fullName()); // "John Doe"
firstName('Jane'); // fullName auto-updates to "Jane Doe"
```
## 🔄 Effects - `$.effect(fn)`
Executes a function and automatically re-runs when its dependencies change.
| Pattern | Example | Description |
|---------|---------|-------------|
| **Basic Effect** | `$.effect(() => console.log(count()))` | Run effect on dependency changes |
| **Cleanup** | `$.effect(() => { timer = setInterval(...); return () => clearInterval(timer) })` | Return cleanup function |
| **Stop Effect** | `const stop = $.effect(...); stop()` | Manually stop an effect |
### Examples
```javascript
// Auto-running effect
const count = $(0);
$.effect(() => {
console.log(`Count is: ${count()}`);
}); // Logs immediately and whenever count changes
// Effect with cleanup
const userId = $(1);
$.effect(() => {
const id = userId();
const timer = setInterval(() => fetchUser(id), 5000);
return () => clearInterval(timer); // Cleanup before re-run
});
```
## 📄 Pages - `$.page(setupFunction)`
Creates a page with automatic cleanup of all signals and effects when navigated away.
```javascript
// pages/about.js
import { $, html } from 'sigpro';
export default $.page(() => {
const count = $(0);
// Auto-cleaned on navigation
$.effect(() => {
document.title = `Count: ${count()}`;
});
return html`
<div>
<h1>About Page</h1>
<p>Count: ${count}</p>
<button @click=${() => count(c => c + 1)}>+</button>
</div>
`;
});
```
### With Parameters
```javascript
export default $.page(({ params, onUnmount }) => {
const userId = params.id;
// Manual cleanup if needed
const interval = setInterval(() => refresh(), 10000);
onUnmount(() => clearInterval(interval));
return html`<div>User: ${userId}</div>`;
});
```
## 🧩 Components - `$.component(tagName, setup, observedAttributes, useShadowDOM)`
Creates Custom Elements with reactive properties.
### Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `tagName` | `string` | required | Custom element tag (must include hyphen) |
| `setupFunction` | `Function` | required | Function that renders the component |
| `observedAttributes` | `string[]` | `[]` | Attributes to observe for changes |
| `useShadowDOM` | `boolean` | `false` | `true` = Shadow DOM (encapsulated), `false` = Light DOM |
### Light DOM Example (Default)
```javascript
// button.js - inherits global styles
$.component('my-button', (props, { slot, emit }) => {
return html`
<button
class="px-4 py-2 bg-blue-500 text-white rounded"
@click=${() => emit('click')}
>
${slot()}
</button>
`;
}, ['variant']); // Observe 'variant' attribute
```
### Shadow DOM Example
```javascript
// calendar.js - encapsulated styles
$.component('my-calendar', (props) => {
return html`
<style>
/* These styles are isolated */
.calendar {
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
</style>
<div class="calendar">
${renderCalendar(props.date())}
</div>
`;
}, ['date'], true); // true = use Shadow DOM
```
## 🌐 Router - `$.router(routes)`
Creates a hash-based router with automatic page cleanup.
### Route Definition
```javascript
const routes = [
// Simple routes
{ path: '/', component: HomePage },
{ path: '/about', component: AboutPage },
// Routes with parameters
{ path: '/user/:id', component: UserPage },
{ path: '/user/:id/posts/:pid', component: PostPage },
// RegExp routes for advanced matching
{ path: /^\/posts\/(?<id>\d+)$/, component: PostPage },
];
```
### Usage
```javascript
import { $, html } from 'sigpro';
import Home from './pages/Home.js';
import User from './pages/User.js';
const router = $.router([
{ path: '/', component: Home },
{ path: '/user/:id', component: User },
]);
// Navigation
$.router.go('/user/42');
$.router.go('about'); // Same as '/about'
// In templates
html`
<nav>
<a href="#/">Home</a>
<a href="#/user/42">Profile</a>
<button @click=${() => $.router.go('/contact')}>
Contact
</button>
</nav>
`;
```
## 📦 Storage - `$.storage(key, initialValue, [storage])`
Persistent signal that syncs with localStorage or sessionStorage.
```javascript
// localStorage (default)
const theme = $.storage('theme', 'light');
const user = $.storage('user', null);
const settings = $.storage('settings', { notifications: true });
// sessionStorage
const tempData = $.storage('temp', {}, sessionStorage);
// Usage like a normal signal
theme('dark'); // Auto-saves to localStorage
console.log(theme()); // 'dark' (even after page refresh)
```
## 🌐 Fetch - `$.fetch(url, data, [loading])`
Simple fetch wrapper with automatic JSON handling.
```javascript
const loading = $(false);
async function loadUser(id) {
const user = await $.fetch(`/api/users/${id}`, null, loading);
if (user) userData(user);
}
// In template
html`
<div>
${() => loading() ? html`<spinner></spinner>` : html`
<p>${userData()?.name}</p>
`}
</div>
`;
```
## 🎨 Template Literals - `` html`...` ``
Creates reactive DOM fragments with directives.
### Directives Reference
| Directive | Example | Description |
|-----------|---------|-------------|
| **Event** | `@click=${handler}` | Add event listener |
| **Two-way binding** | `:value=${signal}` | Bind signal to input value |
| **Boolean attribute** | `?disabled=${signal}` | Toggle boolean attribute |
| **Property** | `.scrollTop=${value}` | Set DOM property directly |
| **Class toggle** | `class:active=${isActive}` | Toggle class conditionally |
### Examples
```javascript
const text = $('');
const isDisabled = $(false);
const activeTab = $('home');
html`
<!-- Event binding -->
<button @click=${() => count(c => c + 1)}>+</button>
<!-- Two-way binding -->
<input :value=${text} />
<p>You typed: ${text}</p>
<!-- Boolean attributes -->
<button ?disabled=${isDisabled}>Submit</button>
<!-- Class toggles -->
<div class:active=${activeTab() === 'home'}>
Home content
</div>
<!-- Property binding -->
<div .scrollTop=${scrollPosition}></div>
`;
```
## 🎯 Complete Component Example
```javascript
import { $, html } from 'sigpro';
// Create a component
$.component('user-profile', (props, { slot, emit }) => {
// Reactive state
const user = $(null);
const loading = $(false);
// Load user data when userId changes
$.effect(() => {
const id = props.userId();
if (id) {
loading(true);
$.fetch(`/api/users/${id}`, null, loading)
.then(data => user(data));
}
});
// Computed value
const fullName = $(() =>
user() ? `${user().firstName} ${user().lastName}` : ''
);
// Template
return html`
<div class="user-profile">
${() => loading() ? html`
<div class="spinner">Loading...</div>
` : user() ? html`
<h2>${fullName}</h2>
<p>Email: ${user().email}</p>
<button @click=${() => emit('select', user())}>
${slot('Select')}
</button>
` : html`
<p>User not found</p>
`}
</div>
`;
}, ['user-id']); // Observe userId attribute
```
<style>
table {
width: 100%;
border-collapse: collapse;
margin: 1.5rem 0;
}
th {
background-color: var(--vp-c-bg-soft);
padding: 0.75rem;
text-align: left;
}
td {
padding: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
tr:hover {
background-color: var(--vp-c-bg-soft);
}
code {
font-size: 0.9em;
padding: 0.2em 0.4em;
border-radius: 4px;
}
</style>

View File

@@ -0,0 +1,784 @@
# Routing API 🌐
SigPro includes a simple yet powerful hash-based router designed for Single Page Applications (SPAs). It works everywhere with zero server configuration and integrates seamlessly with `$.page` for automatic cleanup.
## Why Hash-Based Routing?
Hash routing (`#/about`) works **everywhere** - no server configuration needed. Perfect for:
- Static sites and SPAs
- GitHub Pages, Netlify, any static hosting
- Local development without a server
- Projects that need to work immediately
## `$.router(routes)`
Creates a hash-based router that renders the matching component and handles navigation.
```javascript
import { $, html } from 'sigpro';
import HomePage from './pages/Home.js';
import AboutPage from './pages/About.js';
import UserPage from './pages/User.js';
const routes = [
{ path: '/', component: HomePage },
{ path: '/about', component: AboutPage },
{ path: '/user/:id', component: UserPage },
];
// Mount the router
document.body.appendChild($.router(routes));
```
## 📋 API Reference
### `$.router(routes)`
| Parameter | Type | Description |
|-----------|------|-------------|
| `routes` | `Array<Route>` | Array of route configurations |
**Returns:** `HTMLDivElement` - Container that renders the current page
### `$.router.go(path)`
| Parameter | Type | Description |
|-----------|------|-------------|
| `path` | `string` | Route path to navigate to (automatically adds leading slash) |
### Route Object
| Property | Type | Description |
|----------|------|-------------|
| `path` | `string` or `RegExp` | Route pattern to match |
| `component` | `Function` | Function that returns page content (receives `params`) |
## 🎯 Route Patterns
### String Paths (Simple Routes)
```javascript
const routes = [
// Static routes
{ path: '/', component: HomePage },
{ path: '/about', component: AboutPage },
{ path: '/contact', component: ContactPage },
// Routes with parameters
{ path: '/user/:id', component: UserPage },
{ path: '/user/:id/posts', component: UserPostsPage },
{ path: '/user/:id/posts/:postId', component: PostPage },
{ path: '/search/:query/page/:num', component: SearchPage },
];
```
### RegExp Paths (Advanced Routing)
```javascript
const routes = [
// Match numeric IDs only
{ path: /^\/users\/(?<id>\d+)$/, component: UserPage },
// Match product slugs (letters, numbers, hyphens)
{ path: /^\/products\/(?<slug>[a-z0-9-]+)$/, component: ProductPage },
// Match blog posts by year/month
{ path: /^\/blog\/(?<year>\d{4})\/(?<month>\d{2})$/, component: BlogArchive },
// Match optional language prefix
{ path: /^\/(?<lang>en|es|fr)?\/?about$/, component: AboutPage },
// Match UUID format
{ path: /^\/items\/(?<uuid>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/,
component: ItemPage },
];
```
## 📦 Basic Examples
### Simple Router Setup
```javascript
// main.js
import { $, html } from 'sigpro';
import Home from './pages/Home.js';
import About from './pages/About.js';
import Contact from './pages/Contact.js';
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About },
{ path: '/contact', component: Contact },
];
const router = $.router(routes);
// Mount to DOM
document.body.appendChild(router);
```
### Page Components with Parameters
```javascript
// pages/User.js
import { $, html } from 'sigpro';
export default (params) => $.page(() => {
// /user/42 → params = { id: '42' }
// /user/john/posts/123 → params = { id: 'john', postId: '123' }
const userId = params.id;
const userData = $(null);
$.effect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => userData(data));
});
return html`
<div class="user-page">
<h1>User Profile: ${userId}</h1>
${() => userData() ? html`
<p>Name: ${userData().name}</p>
<p>Email: ${userData().email}</p>
` : html`<p>Loading...</p>`}
</div>
`;
});
```
### Navigation
```javascript
import { $, html } from 'sigpro';
// In templates
const NavBar = () => html`
<nav>
<a href="#/">Home</a>
<a href="#/about">About</a>
<a href="#/contact">Contact</a>
<a href="#/user/42">Profile</a>
<a href="#/search/js/page/1">Search</a>
<!-- Programmatic navigation -->
<button @click=${() => $.router.go('/about')}>
Go to About
</button>
<button @click=${() => $.router.go('contact')}>
Go to Contact (auto-adds leading slash)
</button>
</nav>
`;
```
## 🚀 Advanced Examples
### Complete Application with Layout
```javascript
// App.js
import { $, html } from 'sigpro';
import HomePage from './pages/Home.js';
import AboutPage from './pages/About.js';
import UserPage from './pages/User.js';
import SettingsPage from './pages/Settings.js';
import NotFound from './pages/NotFound.js';
// Layout component with navigation
const Layout = (content) => html`
<div class="app">
<header class="header">
<h1>My SigPro App</h1>
<nav class="nav">
<a href="#/" class:active=${() => isActive('/')}>Home</a>
<a href="#/about" class:active=${() => isActive('/about')}>About</a>
<a href="#/user/42" class:active=${() => isActive('/user/42')}>Profile</a>
<a href="#/settings" class:active=${() => isActive('/settings')}>Settings</a>
</nav>
</header>
<main class="main">
${content}
</main>
<footer class="footer">
<p>© 2024 SigPro App</p>
</footer>
</div>
`;
// Helper to check active route
const isActive = (path) => {
const current = window.location.hash.replace(/^#/, '') || '/';
return current === path;
};
// Routes with layout
const routes = [
{ path: '/', component: (params) => Layout(HomePage(params)) },
{ path: '/about', component: (params) => Layout(AboutPage(params)) },
{ path: '/user/:id', component: (params) => Layout(UserPage(params)) },
{ path: '/settings', component: (params) => Layout(SettingsPage(params)) },
{ path: '/:path(.*)', component: (params) => Layout(NotFound(params)) }, // Catch-all
];
// Create and mount router
const router = $.router(routes);
document.body.appendChild(router);
```
### Nested Routes
```javascript
// pages/Settings.js (parent route)
import { $, html } from 'sigpro';
import SettingsGeneral from './settings/General.js';
import SettingsSecurity from './settings/Security.js';
import SettingsNotifications from './settings/Notifications.js';
export default (params) => $.page(() => {
const section = params.section || 'general';
const sections = {
general: SettingsGeneral,
security: SettingsSecurity,
notifications: SettingsNotifications
};
const CurrentSection = sections[section];
return html`
<div class="settings">
<h1>Settings</h1>
<div class="settings-layout">
<nav class="settings-sidebar">
<a href="#/settings/general" class:active=${() => section === 'general'}>
General
</a>
<a href="#/settings/security" class:active=${() => section === 'security'}>
Security
</a>
<a href="#/settings/notifications" class:active=${() => section === 'notifications'}>
Notifications
</a>
</nav>
<div class="settings-content">
${CurrentSection(params)}
</div>
</div>
</div>
`;
});
// pages/settings/General.js
export default (params) => $.page(() => {
return html`
<div>
<h2>General Settings</h2>
<form>...</form>
</div>
`;
});
// Main router with nested routes
const routes = [
{ path: '/', component: HomePage },
{ path: '/settings/:section?', component: SettingsPage }, // Optional section param
];
```
### Protected Routes (Authentication)
```javascript
// auth.js
import { $ } from 'sigpro';
const isAuthenticated = $(false);
const user = $(null);
export const checkAuth = async () => {
const token = localStorage.getItem('token');
if (token) {
try {
const response = await fetch('/api/verify');
if (response.ok) {
const userData = await response.json();
user(userData);
isAuthenticated(true);
return true;
}
} catch (e) {
// Handle error
}
}
isAuthenticated(false);
user(null);
return false;
};
export const requireAuth = (component) => (params) => {
if (isAuthenticated()) {
return component(params);
}
// Redirect to login
$.router.go('/login');
return null;
};
export { isAuthenticated, user };
```
```javascript
// pages/Dashboard.js (protected route)
import { $, html } from 'sigpro';
import { requireAuth, user } from '../auth.js';
const Dashboard = (params) => $.page(() => {
return html`
<div class="dashboard">
<h1>Welcome, ${() => user()?.name}!</h1>
<p>This is your protected dashboard.</p>
</div>
`;
});
export default requireAuth(Dashboard);
```
```javascript
// main.js with protected routes
import { $, html } from 'sigpro';
import { checkAuth } from './auth.js';
import HomePage from './pages/Home.js';
import LoginPage from './pages/Login.js';
import DashboardPage from './pages/Dashboard.js';
import AdminPage from './pages/Admin.js';
// Check auth on startup
checkAuth();
const routes = [
{ path: '/', component: HomePage },
{ path: '/login', component: LoginPage },
{ path: '/dashboard', component: DashboardPage }, // Protected
{ path: '/admin', component: AdminPage }, // Protected
];
document.body.appendChild($.router(routes));
```
### Route Transitions
```javascript
// with-transitions.js
import { $, html } from 'sigpro';
export const createRouterWithTransitions = (routes) => {
const transitioning = $(false);
const currentView = $(null);
const nextView = $(null);
const container = document.createElement('div');
container.style.display = 'contents';
const renderWithTransition = async (newView) => {
if (currentView() === newView) return;
transitioning(true);
nextView(newView);
// Fade out
container.style.transition = 'opacity 0.2s';
container.style.opacity = '0';
await new Promise(resolve => setTimeout(resolve, 200));
// Update content
container.replaceChildren(newView);
currentView(newView);
// Fade in
container.style.opacity = '1';
await new Promise(resolve => setTimeout(resolve, 200));
transitioning(false);
container.style.transition = '';
};
const router = $.router(routes.map(route => ({
...route,
component: (params) => {
const view = route.component(params);
renderWithTransition(view);
return document.createComment('router-placeholder');
}
})));
return router;
};
```
### Breadcrumbs Navigation
```javascript
// with-breadcrumbs.js
import { $, html } from 'sigpro';
export const createBreadcrumbs = (routes) => {
const breadcrumbs = $([]);
const updateBreadcrumbs = (path) => {
const parts = path.split('/').filter(Boolean);
const crumbs = [];
let currentPath = '';
parts.forEach((part, index) => {
currentPath += `/${part}`;
// Find matching route
const route = routes.find(r => {
if (r.path.includes(':')) {
const pattern = r.path.replace(/:[^/]+/g, part);
return pattern === currentPath;
}
return r.path === currentPath;
});
crumbs.push({
path: currentPath,
label: route?.name || part.charAt(0).toUpperCase() + part.slice(1),
isLast: index === parts.length - 1
});
});
breadcrumbs(crumbs);
};
// Listen to route changes
window.addEventListener('hashchange', () => {
const path = window.location.hash.replace(/^#/, '') || '/';
updateBreadcrumbs(path);
});
// Initial update
updateBreadcrumbs(window.location.hash.replace(/^#/, '') || '/');
return breadcrumbs;
};
```
```javascript
// Usage in layout
import { createBreadcrumbs } from './with-breadcrumbs.js';
const breadcrumbs = createBreadcrumbs(routes);
const Layout = (content) => html`
<div class="app">
<nav class="breadcrumbs">
${() => breadcrumbs().map(crumb => html`
${!crumb.isLast ? html`
<a href="#${crumb.path}">${crumb.label}</a>
<span class="separator">/</span>
` : html`
<span class="current">${crumb.label}</span>
`}
`)}
</nav>
<main>
${content}
</main>
</div>
`;
```
### Query Parameters
```javascript
// with-query-params.js
export const getQueryParams = () => {
const hash = window.location.hash;
const queryStart = hash.indexOf('?');
if (queryStart === -1) return {};
const queryString = hash.slice(queryStart + 1);
const params = new URLSearchParams(queryString);
const result = {};
for (const [key, value] of params) {
result[key] = value;
}
return result;
};
export const updateQueryParams = (params) => {
const hash = window.location.hash.split('?')[0];
const queryString = new URLSearchParams(params).toString();
window.location.hash = queryString ? `${hash}?${queryString}` : hash;
};
```
```javascript
// Search page with query params
import { $, html } from 'sigpro';
import { getQueryParams, updateQueryParams } from './with-query-params.js';
export default (params) => $.page(() => {
// Get initial query from URL
const queryParams = getQueryParams();
const searchQuery = $(queryParams.q || '');
const page = $(parseInt(queryParams.page) || 1);
const results = $([]);
// Update URL when search changes
$.effect(() => {
updateQueryParams({
q: searchQuery() || undefined,
page: page() > 1 ? page() : undefined
});
});
// Fetch results when search or page changes
$.effect(() => {
if (searchQuery()) {
fetch(`/api/search?q=${searchQuery()}&page=${page()}`)
.then(res => res.json())
.then(data => results(data));
}
});
return html`
<div class="search-page">
<h1>Search</h1>
<input
type="search"
:value=${searchQuery}
placeholder="Search..."
@input=${(e) => {
searchQuery(e.target.value);
page(1); // Reset to first page on new search
}}
/>
<div class="results">
${results().map(item => html`
<div class="result">${item.title}</div>
`)}
</div>
${() => results().length ? html`
<div class="pagination">
<button
?disabled=${() => page() <= 1}
@click=${() => page(p => p - 1)}
>
Previous
</button>
<span>Page ${page}</span>
<button
?disabled=${() => results().length < 10}
@click=${() => page(p => p + 1)}
>
Next
</button>
</div>
` : ''}
</div>
`;
});
```
### Lazy Loading Routes
```javascript
// lazy.js
export const lazy = (loader) => {
let component = null;
return async (params) => {
if (!component) {
const module = await loader();
component = module.default;
}
return component(params);
};
};
```
```javascript
// main.js with lazy loading
import { $, html } from 'sigpro';
import { lazy } from './lazy.js';
import Layout from './Layout.js';
const routes = [
{ path: '/', component: lazy(() => import('./pages/Home.js')) },
{ path: '/about', component: lazy(() => import('./pages/About.js')) },
{ path: '/dashboard', component: lazy(() => import('./pages/Dashboard.js')) },
{
path: '/admin',
component: lazy(() => import('./pages/Admin.js')),
// Show loading state
loading: () => html`<div class="loading">Loading admin panel...</div>`
},
];
// Wrap with layout
const routesWithLayout = routes.map(route => ({
...route,
component: (params) => Layout(route.component(params))
}));
document.body.appendChild($.router(routesWithLayout));
```
### Route Guards / Middleware
```javascript
// middleware.js
export const withGuard = (component, guard) => (params) => {
const result = guard(params);
if (result === true) {
return component(params);
} else if (typeof result === 'string') {
$.router.go(result);
return null;
}
return result; // Custom component (e.g., AccessDenied)
};
// Guards
export const roleGuard = (requiredRole) => (params) => {
const userRole = localStorage.getItem('userRole');
if (userRole === requiredRole) return true;
if (!userRole) return '/login';
return AccessDeniedPage(params);
};
export const authGuard = () => (params) => {
const token = localStorage.getItem('token');
return token ? true : '/login';
};
export const pendingChangesGuard = (hasPendingChanges) => (params) => {
if (hasPendingChanges()) {
return ConfirmLeavePage(params);
}
return true;
};
```
```javascript
// Usage
import { withGuard, authGuard, roleGuard } from './middleware.js';
const routes = [
{ path: '/', component: HomePage },
{ path: '/profile', component: withGuard(ProfilePage, authGuard()) },
{
path: '/admin',
component: withGuard(AdminPage, roleGuard('admin'))
},
];
```
## 📊 Route Matching Priority
Routes are matched in the order they are defined. More specific routes should come first:
```javascript
const routes = [
// More specific first
{ path: '/user/:id/edit', component: EditUserPage },
{ path: '/user/:id/posts', component: UserPostsPage },
{ path: '/user/:id', component: UserPage },
// Static routes
{ path: '/about', component: AboutPage },
{ path: '/contact', component: ContactPage },
// Catch-all last
{ path: '/:path(.*)', component: NotFoundPage },
];
```
## 🎯 Complete Example
```javascript
// main.js - Complete application
import { $, html } from 'sigpro';
import { lazy } from './utils/lazy.js';
import { withGuard, authGuard } from './utils/middleware.js';
import Layout from './components/Layout.js';
// Lazy load pages
const HomePage = lazy(() => import('./pages/Home.js'));
const AboutPage = lazy(() => import('./pages/About.js'));
const LoginPage = lazy(() => import('./pages/Login.js'));
const DashboardPage = lazy(() => import('./pages/Dashboard.js'));
const UserPage = lazy(() => import('./pages/User.js'));
const SettingsPage = lazy(() => import('./pages/Settings.js'));
const NotFoundPage = lazy(() => import('./pages/NotFound.js'));
// Route configuration
const routes = [
{ path: '/', component: HomePage, name: 'Home' },
{ path: '/about', component: AboutPage, name: 'About' },
{ path: '/login', component: LoginPage, name: 'Login' },
{
path: '/dashboard',
component: withGuard(DashboardPage, authGuard()),
name: 'Dashboard'
},
{
path: '/user/:id',
component: UserPage,
name: 'User Profile'
},
{
path: '/settings/:section?',
component: withGuard(SettingsPage, authGuard()),
name: 'Settings'
},
{ path: '/:path(.*)', component: NotFoundPage, name: 'Not Found' },
];
// Wrap all routes with layout
const routesWithLayout = routes.map(route => ({
...route,
component: (params) => Layout(route.component(params))
}));
// Create and mount router
const router = $.router(routesWithLayout);
document.body.appendChild(router);
// Navigation helper (available globally)
window.navigate = $.router.go;
```
## 📊 Summary
| Feature | Description |
|---------|-------------|
| **Hash-based** | Works everywhere, no server config |
| **Route Parameters** | `:param` syntax for dynamic segments |
| **RegExp Support** | Advanced pattern matching |
| **Query Parameters** | Support for `?key=value` in URLs |
| **Programmatic Navigation** | `$.router.go(path)` |
| **Auto-cleanup** | Works with `$.page` for memory management |
| **Zero Dependencies** | Pure vanilla JavaScript |
| **Lazy Loading Ready** | Easy code splitting |
---
> **Pro Tip:** Order matters in route definitions - put more specific routes (with parameters) before static ones, and always include a catch-all route (404) at the end.

View File

@@ -0,0 +1,899 @@
# Signals API 📡
Signals are the heart of SigPro's reactivity system. They are reactive values that automatically track dependencies and notify subscribers when they change. This enables fine-grained updates without virtual DOM diffing.
## Core Concepts
### What is a Signal?
A signal is a function that holds a value and notifies dependents when that value changes. Signals can be:
- **Basic signals** - Hold simple values (numbers, strings, objects)
- **Computed signals** - Derive values from other signals
- **Persistent signals** - Automatically sync with localStorage/sessionStorage
### How Reactivity Works
SigPro uses automatic dependency tracking:
1. When you read a signal inside an effect, the effect becomes a subscriber
2. When the signal's value changes, all subscribers are notified
3. Updates are batched using microtasks for optimal performance
4. Only the exact nodes that depend on changed values are updated
## `$(initialValue)`
Creates a reactive signal. The behavior changes based on the type of `initialValue`:
- If `initialValue` is a **function**, creates a computed signal
- Otherwise, creates a basic signal
```javascript
import { $ } from 'sigpro';
// Basic signal
const count = $(0);
// Computed signal
const firstName = $('John');
const lastName = $('Doe');
const fullName = $(() => `${firstName()} ${lastName()}`);
```
## 📋 API Reference
### Basic Signals
| Pattern | Example | Description |
|---------|---------|-------------|
| Create | `const count = $(0)` | Create signal with initial value |
| Get | `count()` | Read current value |
| Set | `count(5)` | Set new value directly |
| Update | `count(prev => prev + 1)` | Update based on previous value |
### Computed Signals
| Pattern | Example | Description |
|---------|---------|-------------|
| Create | `const total = $(() => price() * quantity())` | Derive value from other signals |
| Get | `total()` | Read computed value (auto-updates) |
### Signal Methods
| Method | Description | Example |
|--------|-------------|---------|
| `signal()` | Gets current value | `count()` |
| `signal(newValue)` | Sets new value | `count(5)` |
| `signal(prev => new)` | Updates using previous value | `count(c => c + 1)` |
## 🎯 Basic Examples
### Counter Signal
```javascript
import { $ } from 'sigpro';
const count = $(0);
console.log(count()); // 0
count(5);
console.log(count()); // 5
count(prev => prev + 1);
console.log(count()); // 6
```
### Object Signal
```javascript
import { $ } from 'sigpro';
const user = $({
name: 'John',
age: 30,
email: 'john@example.com'
});
// Read
console.log(user().name); // 'John'
// Update (immutable pattern)
user({
...user(),
age: 31
});
// Partial update with function
user(prev => ({
...prev,
email: 'john.doe@example.com'
}));
```
### Array Signal
```javascript
import { $ } from 'sigpro';
const todos = $(['Learn SigPro', 'Build an app']);
// Add item
todos([...todos(), 'Deploy to production']);
// Remove item
todos(todos().filter((_, i) => i !== 1));
// Update item
todos(todos().map((todo, i) =>
i === 0 ? 'Master SigPro' : todo
));
```
## 🔄 Computed Signals
Computed signals automatically update when their dependencies change:
```javascript
import { $ } from 'sigpro';
const price = $(10);
const quantity = $(2);
const tax = $(0.21);
// Computed signals
const subtotal = $(() => price() * quantity());
const taxAmount = $(() => subtotal() * tax());
const total = $(() => subtotal() + taxAmount());
console.log(total()); // 24.2
price(15);
console.log(total()); // 36.3 (automatically updated)
quantity(3);
console.log(total()); // 54.45 (automatically updated)
```
### Computed with Multiple Dependencies
```javascript
import { $ } from 'sigpro';
const firstName = $('John');
const lastName = $('Doe');
const prefix = $('Mr.');
const fullName = $(() => {
// Computed signals can contain logic
const name = `${firstName()} ${lastName()}`;
return prefix() ? `${prefix()} ${name}` : name;
});
console.log(fullName()); // 'Mr. John Doe'
prefix('');
console.log(fullName()); // 'John Doe'
```
### Computed with Conditional Logic
```javascript
import { $ } from 'sigpro';
const user = $({ role: 'admin', permissions: [] });
const isAdmin = $(() => user().role === 'admin');
const hasPermission = $(() =>
isAdmin() || user().permissions.includes('edit')
);
console.log(hasPermission()); // true
user({ role: 'user', permissions: ['view'] });
console.log(hasPermission()); // false (can't edit)
user({ role: 'user', permissions: ['view', 'edit'] });
console.log(hasPermission()); // true (now has permission)
```
## 🧮 Advanced Signal Patterns
### Derived State Pattern
```javascript
import { $ } from 'sigpro';
// Shopping cart example
const cart = $([
{ id: 1, name: 'Product 1', price: 10, quantity: 2 },
{ id: 2, name: 'Product 2', price: 15, quantity: 1 },
]);
// Derived values
const itemCount = $(() =>
cart().reduce((sum, item) => sum + item.quantity, 0)
);
const subtotal = $(() =>
cart().reduce((sum, item) => sum + (item.price * item.quantity), 0)
);
const tax = $(() => subtotal() * 0.21);
const total = $(() => subtotal() + tax());
// Update cart
cart([
...cart(),
{ id: 3, name: 'Product 3', price: 20, quantity: 1 }
]);
// All derived values auto-update
console.log(itemCount()); // 4
console.log(total()); // (10*2 + 15*1 + 20*1) * 1.21 = 78.65
```
### Validation Pattern
```javascript
import { $ } from 'sigpro';
const email = $('');
const password = $('');
const confirmPassword = $('');
// Validation signals
const isEmailValid = $(() => {
const value = email();
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
});
const isPasswordValid = $(() => {
const value = password();
return value.length >= 8;
});
const doPasswordsMatch = $(() =>
password() === confirmPassword()
);
const isFormValid = $(() =>
isEmailValid() && isPasswordValid() && doPasswordsMatch()
);
// Update form
email('user@example.com');
password('secure123');
confirmPassword('secure123');
console.log(isFormValid()); // true
// Validation messages
const emailError = $(() =>
email() && !isEmailValid() ? 'Invalid email format' : ''
);
```
### Filtering and Search Pattern
```javascript
import { $ } from 'sigpro';
const items = $([
{ id: 1, name: 'Apple', category: 'fruit' },
{ id: 2, name: 'Banana', category: 'fruit' },
{ id: 3, name: 'Carrot', category: 'vegetable' },
{ id: 4, name: 'Date', category: 'fruit' },
]);
const searchTerm = $('');
const categoryFilter = $('all');
// Filtered items (computed)
const filteredItems = $(() => {
let result = items();
// Apply search filter
if (searchTerm()) {
const term = searchTerm().toLowerCase();
result = result.filter(item =>
item.name.toLowerCase().includes(term)
);
}
// Apply category filter
if (categoryFilter() !== 'all') {
result = result.filter(item =>
item.category === categoryFilter()
);
}
return result;
});
// Stats
const fruitCount = $(() =>
items().filter(item => item.category === 'fruit').length
);
const vegCount = $(() =>
items().filter(item => item.category === 'vegetable').length
);
// Update filters
searchTerm('a');
console.log(filteredItems().map(i => i.name)); // ['Apple', 'Banana', 'Carrot', 'Date']
categoryFilter('fruit');
console.log(filteredItems().map(i => i.name)); // ['Apple', 'Banana', 'Date']
```
### Pagination Pattern
```javascript
import { $ } from 'sigpro';
const allItems = $([...Array(100).keys()].map(i => `Item ${i + 1}`));
const currentPage = $(1);
const itemsPerPage = $(10);
// Paginated items (computed)
const paginatedItems = $(() => {
const start = (currentPage() - 1) * itemsPerPage();
const end = start + itemsPerPage();
return allItems().slice(start, end);
});
// Pagination metadata
const totalPages = $(() =>
Math.ceil(allItems().length / itemsPerPage())
);
const hasNextPage = $(() =>
currentPage() < totalPages()
);
const hasPrevPage = $(() =>
currentPage() > 1
);
const pageRange = $(() => {
const current = currentPage();
const total = totalPages();
const delta = 2;
let range = [];
for (let i = Math.max(2, current - delta);
i <= Math.min(total - 1, current + delta);
i++) {
range.push(i);
}
if (current - delta > 2) range = ['...', ...range];
if (current + delta < total - 1) range = [...range, '...'];
return [1, ...range, total];
});
// Navigation
const nextPage = () => {
if (hasNextPage()) currentPage(c => c + 1);
};
const prevPage = () => {
if (hasPrevPage()) currentPage(c => c - 1);
};
const goToPage = (page) => {
if (page >= 1 && page <= totalPages()) {
currentPage(page);
}
};
```
## 🔧 Advanced Signal Features
### Signal Equality Comparison
Signals use `Object.is` for change detection. Only notify subscribers when values are actually different:
```javascript
import { $ } from 'sigpro';
const count = $(0);
// These won't trigger updates:
count(0); // Same value
count(prev => prev); // Returns same value
// These will trigger updates:
count(1); // Different value
count(prev => prev + 0); // Still 0? Actually returns 0? Wait...
// Be careful with functional updates!
```
### Batch Updates
Multiple signal updates are batched into a single microtask:
```javascript
import { $ } from 'sigpro';
const firstName = $('John');
const lastName = $('Doe');
const fullName = $(() => `${firstName()} ${lastName()}`);
$.effect(() => {
console.log('Full name:', fullName());
});
// Logs: 'Full name: John Doe'
// Multiple updates in same tick - only one effect run!
firstName('Jane');
lastName('Smith');
// Only logs once: 'Full name: Jane Smith'
```
### Infinite Loop Protection
SigPro includes protection against infinite reactive loops:
```javascript
import { $ } from 'sigpro';
const a = $(1);
const b = $(2);
// This would create a loop, but SigPro prevents it
$.effect(() => {
a(b()); // Reading b
b(a()); // Reading a - loop detected!
});
// Throws: "SigPro: Infinite reactive loop detected."
```
## 📊 Performance Characteristics
| Operation | Complexity | Notes |
|-----------|------------|-------|
| Signal read | O(1) | Direct value access |
| Signal write | O(n) | n = number of subscribers |
| Computed read | O(1) or O(m) | m = computation complexity |
| Effect run | O(s) | s = number of signal reads |
## 🎯 Best Practices
### 1. Keep Signals Focused
```javascript
// ❌ Avoid large monolithic signals
const state = $({
user: null,
posts: [],
theme: 'light',
notifications: []
});
// ✅ Split into focused signals
const user = $(null);
const posts = $([]);
const theme = $('light');
const notifications = $([]);
```
### 2. Use Computed for Derived State
```javascript
// ❌ Don't compute in templates/effects
$.effect(() => {
const total = items().reduce((sum, i) => sum + i.price, 0);
updateUI(total);
});
// ✅ Compute with signals
const total = $(() => items().reduce((sum, i) => sum + i.price, 0));
$.effect(() => updateUI(total()));
```
### 3. Immutable Updates
```javascript
// ❌ Don't mutate objects/arrays
const user = $({ name: 'John' });
user().name = 'Jane'; // Won't trigger updates!
// ✅ Create new objects/arrays
user({ ...user(), name: 'Jane' });
// ❌ Don't mutate arrays
const todos = $(['a', 'b']);
todos().push('c'); // Won't trigger updates!
// ✅ Create new arrays
todos([...todos(), 'c']);
```
### 4. Functional Updates for Dependencies
```javascript
// ❌ Avoid if new value depends on current
count(count() + 1);
// ✅ Use functional update
count(prev => prev + 1);
```
### 5. Clean Up Effects
```javascript
import { $ } from 'sigpro';
const userId = $(1);
// Effects auto-clean in pages, but you can stop manually
const stop = $.effect(() => {
fetchUser(userId());
});
// Later, if needed
stop();
```
## 🚀 Real-World Examples
### Form State Management
```javascript
import { $ } from 'sigpro';
// Form state
const formData = $({
username: '',
email: '',
age: '',
newsletter: false
});
// Touched fields (for validation UI)
const touched = $({
username: false,
email: false,
age: false
});
// Validation rules
const validations = {
username: (value) =>
value.length >= 3 ? null : 'Username must be at least 3 characters',
email: (value) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? null : 'Invalid email',
age: (value) =>
!value || (value >= 18 && value <= 120) ? null : 'Age must be 18-120'
};
// Validation signals
const errors = $(() => {
const data = formData();
const result = {};
Object.keys(validations).forEach(field => {
const error = validations[field](data[field]);
if (error) result[field] = error;
});
return result;
});
const isValid = $(() => Object.keys(errors()).length === 0);
// Field helpers
const fieldProps = (field) => ({
value: formData()[field],
error: touched()[field] ? errors()[field] : null,
onChange: (e) => {
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
formData({
...formData(),
[field]: value
});
},
onBlur: () => {
touched({
...touched(),
[field]: true
});
}
});
// Form submission
const submitAttempts = $(0);
const isSubmitting = $(false);
const handleSubmit = async () => {
submitAttempts(s => s + 1);
if (!isValid()) {
// Mark all fields as touched to show errors
touched(Object.keys(formData()).reduce((acc, field) => ({
...acc,
[field]: true
}), {}));
return;
}
isSubmitting(true);
try {
await saveForm(formData());
// Reset form on success
formData({ username: '', email: '', age: '', newsletter: false });
touched({ username: false, email: false, age: false });
} finally {
isSubmitting(false);
}
};
```
### Todo App with Filters
```javascript
import { $ } from 'sigpro';
// State
const todos = $([
{ id: 1, text: 'Learn SigPro', completed: true },
{ id: 2, text: 'Build an app', completed: false },
{ id: 3, text: 'Write docs', completed: false }
]);
const filter = $('all'); // 'all', 'active', 'completed'
const newTodoText = $('');
// Computed values
const filteredTodos = $(() => {
const all = todos();
switch(filter()) {
case 'active':
return all.filter(t => !t.completed);
case 'completed':
return all.filter(t => t.completed);
default:
return all;
}
});
const activeCount = $(() =>
todos().filter(t => !t.completed).length
);
const completedCount = $(() =>
todos().filter(t => t.completed).length
);
const hasCompleted = $(() => completedCount() > 0);
// Actions
const addTodo = () => {
const text = newTodoText().trim();
if (text) {
todos([
...todos(),
{
id: Date.now(),
text,
completed: false
}
]);
newTodoText('');
}
};
const toggleTodo = (id) => {
todos(todos().map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
));
};
const deleteTodo = (id) => {
todos(todos().filter(todo => todo.id !== id));
};
const clearCompleted = () => {
todos(todos().filter(todo => !todo.completed));
};
const toggleAll = () => {
const allCompleted = activeCount() === 0;
todos(todos().map(todo => ({
...todo,
completed: !allCompleted
})));
};
```
### Shopping Cart
```javascript
import { $ } from 'sigpro';
// Products catalog
const products = $([
{ id: 1, name: 'Laptop', price: 999, stock: 5 },
{ id: 2, name: 'Mouse', price: 29, stock: 20 },
{ id: 3, name: 'Keyboard', price: 79, stock: 10 },
{ id: 4, name: 'Monitor', price: 299, stock: 3 }
]);
// Cart state
const cart = $({});
const selectedProduct = $(null);
const quantity = $(1);
// Computed cart values
const cartItems = $(() => {
const items = [];
Object.entries(cart()).forEach(([productId, qty]) => {
const product = products().find(p => p.id === parseInt(productId));
if (product) {
items.push({
...product,
quantity: qty,
subtotal: product.price * qty
});
}
});
return items;
});
const itemCount = $(() =>
cartItems().reduce((sum, item) => sum + item.quantity, 0)
);
const subtotal = $(() =>
cartItems().reduce((sum, item) => sum + item.subtotal, 0)
);
const tax = $(() => subtotal() * 0.10);
const shipping = $(() => subtotal() > 100 ? 0 : 10);
const total = $(() => subtotal() + tax() + shipping());
const isCartEmpty = $(() => itemCount() === 0);
// Cart actions
const addToCart = (product, qty = 1) => {
const currentQty = cart()[product.id] || 0;
const newQty = currentQty + qty;
if (newQty <= product.stock) {
cart({
...cart(),
[product.id]: newQty
});
return true;
}
return false;
};
const updateQuantity = (productId, newQty) => {
const product = products().find(p => p.id === productId);
if (newQty <= product.stock) {
if (newQty <= 0) {
removeFromCart(productId);
} else {
cart({
...cart(),
[productId]: newQty
});
}
}
};
const removeFromCart = (productId) => {
const newCart = { ...cart() };
delete newCart[productId];
cart(newCart);
};
const clearCart = () => cart({});
// Stock management
const productStock = (productId) => {
const product = products().find(p => p.id === productId);
if (!product) return 0;
const inCart = cart()[productId] || 0;
return product.stock - inCart;
};
const isInStock = (productId, qty = 1) => {
return productStock(productId) >= qty;
};
```
## 📈 Debugging Signals
### Logging Signal Changes
```javascript
import { $ } from 'sigpro';
// Wrap a signal to log changes
const withLogging = (signal, name) => {
return (...args) => {
if (args.length) {
const oldValue = signal();
const result = signal(...args);
console.log(`${name}:`, oldValue, '->', signal());
return result;
}
return signal();
};
};
// Usage
const count = withLogging($(0), 'count');
count(5); // Logs: "count: 0 -> 5"
```
### Signal Inspector
```javascript
import { $ } from 'sigpro';
// Create an inspectable signal
const createInspector = () => {
const signals = new Map();
const createSignal = (initialValue, name) => {
const signal = $(initialValue);
signals.set(signal, { name, subscribers: new Set() });
// Wrap to track subscribers
const wrapped = (...args) => {
if (!args.length && activeEffect) {
const info = signals.get(wrapped);
info.subscribers.add(activeEffect);
}
return signal(...args);
};
return wrapped;
};
const getInfo = () => {
const info = {};
signals.forEach((data, signal) => {
info[data.name] = {
subscribers: data.subscribers.size,
value: signal()
};
});
return info;
};
return { createSignal, getInfo };
};
// Usage
const inspector = createInspector();
const count = inspector.createSignal(0, 'count');
const doubled = inspector.createSignal(() => count() * 2, 'doubled');
console.log(inspector.getInfo());
// { count: { subscribers: 0, value: 0 }, doubled: { subscribers: 0, value: 0 } }
```
## 📊 Summary
| Feature | Description |
|---------|-------------|
| **Basic Signals** | Hold values and notify on change |
| **Computed Signals** | Auto-updating derived values |
| **Automatic Tracking** | Dependencies tracked automatically |
| **Batch Updates** | Multiple updates batched in microtask |
| **Infinite Loop Protection** | Prevents reactive cycles |
| **Zero Dependencies** | Pure vanilla JavaScript |
---
> **Pro Tip:** Signals are the foundation of reactivity in SigPro. Master them, and you've mastered 80% of the library!

View File

@@ -0,0 +1,952 @@
# Storage API 💾
SigPro provides persistent signals that automatically synchronize with browser storage APIs. This allows you to create reactive state that survives page reloads and browser sessions with zero additional code.
## Core Concepts
### What is Persistent Storage?
Persistent signals are special signals that:
- **Initialize from storage** (localStorage/sessionStorage) if a saved value exists
- **Auto-save** whenever the signal value changes
- **Handle JSON serialization** automatically
- **Clean up** when set to `null` or `undefined`
### Storage Types
| Storage | Persistence | Use Case |
|---------|-------------|----------|
| `localStorage` | Forever (until cleared) | User preferences, themes, saved data |
| `sessionStorage` | Until tab/window closes | Form drafts, temporary state |
## `$.storage(key, initialValue, [storage])`
Creates a persistent signal that syncs with browser storage.
```javascript
import { $ } from 'sigpro';
// localStorage (default)
const theme = $.storage('theme', 'light');
const user = $.storage('user', null);
const settings = $.storage('settings', { notifications: true });
// sessionStorage
const draft = $.storage('draft', '', sessionStorage);
const formData = $.storage('form', {}, sessionStorage);
```
## 📋 API Reference
### Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `key` | `string` | required | Storage key name |
| `initialValue` | `any` | required | Default value if none stored |
| `storage` | `Storage` | `localStorage` | Storage type (`localStorage` or `sessionStorage`) |
### Returns
| Return | Description |
|--------|-------------|
| `Function` | Signal function (getter/setter) with persistence |
## 🎯 Basic Examples
### Theme Preference
```javascript
import { $, html } from 'sigpro';
// Persistent theme signal
const theme = $.storage('theme', 'light');
// Apply theme to document
$.effect(() => {
document.body.className = `theme-${theme()}`;
});
// Toggle theme
const toggleTheme = () => {
theme(t => t === 'light' ? 'dark' : 'light');
};
// Template
html`
<div>
<p>Current theme: ${theme}</p>
<button @click=${toggleTheme}>
Toggle Theme
</button>
</div>
`;
```
### User Preferences
```javascript
import { $ } from 'sigpro';
// Complex preferences object
const preferences = $.storage('preferences', {
language: 'en',
fontSize: 'medium',
notifications: true,
compactView: false,
sidebarOpen: true
});
// Update single preference
const setPreference = (key, value) => {
preferences({
...preferences(),
[key]: value
});
};
// Usage
setPreference('language', 'es');
setPreference('fontSize', 'large');
console.log(preferences().language); // 'es'
```
### Form Draft
```javascript
import { $, html } from 'sigpro';
// Session-based draft (clears when tab closes)
const draft = $.storage('contact-form', {
name: '',
email: '',
message: ''
}, sessionStorage);
// Auto-save on input
const handleInput = (field, value) => {
draft({
...draft(),
[field]: value
});
};
// Clear draft after submit
const handleSubmit = async () => {
await submitForm(draft());
draft(null); // Clears from storage
};
// Template
html`
<form @submit=${handleSubmit}>
<input
type="text"
:value=${() => draft().name}
@input=${(e) => handleInput('name', e.target.value)}
placeholder="Name"
/>
<input
type="email"
:value=${() => draft().email}
@input=${(e) => handleInput('email', e.target.value)}
placeholder="Email"
/>
<textarea
:value=${() => draft().message}
@input=${(e) => handleInput('message', e.target.value)}
placeholder="Message"
></textarea>
<button type="submit">Send</button>
</form>
`;
```
## 🚀 Advanced Examples
### Authentication State
```javascript
import { $, html } from 'sigpro';
// Persistent auth state
const auth = $.storage('auth', {
token: null,
user: null,
expiresAt: null
});
// Computed helpers
const isAuthenticated = $(() => {
const { token, expiresAt } = auth();
if (!token || !expiresAt) return false;
return new Date(expiresAt) > new Date();
});
const user = $(() => auth().user);
// Login function
const login = async (email, password) => {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password })
});
if (response.ok) {
const { token, user, expiresIn } = await response.json();
auth({
token,
user,
expiresAt: new Date(Date.now() + expiresIn * 1000).toISOString()
});
return true;
}
return false;
};
// Logout
const logout = () => {
auth(null); // Clear from storage
};
// Auto-refresh token
$.effect(() => {
if (!isAuthenticated()) return;
const { expiresAt } = auth();
const expiresIn = new Date(expiresAt) - new Date();
const refreshTime = expiresIn - 60000; // 1 minute before expiry
if (refreshTime > 0) {
const timer = setTimeout(refreshToken, refreshTime);
return () => clearTimeout(timer);
}
});
// Navigation guard
$.effect(() => {
if (!isAuthenticated() && window.location.pathname !== '/login') {
$.router.go('/login');
}
});
```
### Multi-tab Synchronization
```javascript
import { $ } from 'sigpro';
// Storage key for cross-tab communication
const STORAGE_KEY = 'app-state';
// Create persistent signal
const appState = $.storage(STORAGE_KEY, {
count: 0,
lastUpdated: null
});
// Listen for storage events (changes from other tabs)
window.addEventListener('storage', (event) => {
if (event.key === STORAGE_KEY && event.newValue) {
try {
// Update signal without triggering save loop
const newValue = JSON.parse(event.newValue);
appState(newValue);
} catch (e) {
console.error('Failed to parse storage event:', e);
}
}
});
// Update state (syncs across all tabs)
const increment = () => {
appState({
count: appState().count + 1,
lastUpdated: new Date().toISOString()
});
};
// Tab counter
const tabCount = $(1);
// Track number of tabs open
window.addEventListener('storage', (event) => {
if (event.key === 'tab-heartbeat') {
tabCount(parseInt(event.newValue) || 1);
}
});
// Send heartbeat
setInterval(() => {
localStorage.setItem('tab-heartbeat', tabCount());
}, 1000);
```
### Settings Manager
```javascript
import { $, html } from 'sigpro';
// Settings schema
const settingsSchema = {
theme: {
type: 'select',
options: ['light', 'dark', 'system'],
default: 'system'
},
fontSize: {
type: 'range',
min: 12,
max: 24,
default: 16
},
notifications: {
type: 'checkbox',
default: true
},
language: {
type: 'select',
options: ['en', 'es', 'fr', 'de'],
default: 'en'
}
};
// Persistent settings
const settings = $.storage('app-settings',
Object.entries(settingsSchema).reduce((acc, [key, config]) => ({
...acc,
[key]: config.default
}), {})
);
// Settings component
const SettingsPanel = () => {
return html`
<div class="settings-panel">
<h2>Settings</h2>
${Object.entries(settingsSchema).map(([key, config]) => {
switch(config.type) {
case 'select':
return html`
<div class="setting">
<label>${key}:</label>
<select
:value=${() => settings()[key]}
@change=${(e) => updateSetting(key, e.target.value)}
>
${config.options.map(opt => html`
<option value="${opt}" ?selected=${() => settings()[key] === opt}>
${opt}
</option>
`)}
</select>
</div>
`;
case 'range':
return html`
<div class="setting">
<label>${key}: ${() => settings()[key]}</label>
<input
type="range"
min="${config.min}"
max="${config.max}"
:value=${() => settings()[key]}
@input=${(e) => updateSetting(key, parseInt(e.target.value))}
/>
</div>
`;
case 'checkbox':
return html`
<div class="setting">
<label>
<input
type="checkbox"
:checked=${() => settings()[key]}
@change=${(e) => updateSetting(key, e.target.checked)}
/>
${key}
</label>
</div>
`;
}
})}
<button @click=${resetDefaults}>Reset to Defaults</button>
</div>
`;
};
// Helper functions
const updateSetting = (key, value) => {
settings({
...settings(),
[key]: value
});
};
const resetDefaults = () => {
const defaults = Object.entries(settingsSchema).reduce((acc, [key, config]) => ({
...acc,
[key]: config.default
}), {});
settings(defaults);
};
// Apply settings globally
$.effect(() => {
const { theme, fontSize } = settings();
// Apply theme
document.documentElement.setAttribute('data-theme', theme);
// Apply font size
document.documentElement.style.fontSize = `${fontSize}px`;
});
```
### Shopping Cart Persistence
```javascript
import { $, html } from 'sigpro';
// Persistent shopping cart
const cart = $.storage('shopping-cart', {
items: [],
lastUpdated: null
});
// Computed values
const cartItems = $(() => cart().items);
const itemCount = $(() => cartItems().reduce((sum, item) => sum + item.quantity, 0));
const subtotal = $(() => cartItems().reduce((sum, item) => sum + (item.price * item.quantity), 0));
const tax = $(() => subtotal() * 0.1);
const total = $(() => subtotal() + tax());
// Cart actions
const addToCart = (product, quantity = 1) => {
const existing = cartItems().findIndex(item => item.id === product.id);
if (existing >= 0) {
// Update quantity
const newItems = [...cartItems()];
newItems[existing] = {
...newItems[existing],
quantity: newItems[existing].quantity + quantity
};
cart({
items: newItems,
lastUpdated: new Date().toISOString()
});
} else {
// Add new item
cart({
items: [...cartItems(), { ...product, quantity }],
lastUpdated: new Date().toISOString()
});
}
};
const removeFromCart = (productId) => {
cart({
items: cartItems().filter(item => item.id !== productId),
lastUpdated: new Date().toISOString()
});
};
const updateQuantity = (productId, quantity) => {
if (quantity <= 0) {
removeFromCart(productId);
} else {
const newItems = cartItems().map(item =>
item.id === productId ? { ...item, quantity } : item
);
cart({
items: newItems,
lastUpdated: new Date().toISOString()
});
}
};
const clearCart = () => {
cart({
items: [],
lastUpdated: new Date().toISOString()
});
};
// Cart expiration (7 days)
const CART_EXPIRY_DAYS = 7;
$.effect(() => {
const lastUpdated = cart().lastUpdated;
if (lastUpdated) {
const expiryDate = new Date(lastUpdated);
expiryDate.setDate(expiryDate.getDate() + CART_EXPIRY_DAYS);
if (new Date() > expiryDate) {
clearCart();
}
}
});
// Cart display component
const CartDisplay = () => html`
<div class="cart">
<h3>Shopping Cart (${itemCount} items)</h3>
${cartItems().map(item => html`
<div class="cart-item">
<span>${item.name}</span>
<span>$${item.price} x ${item.quantity}</span>
<span>$${item.price * item.quantity}</span>
<button @click=${() => removeFromCart(item.id)}>Remove</button>
<input
type="number"
min="1"
:value=${item.quantity}
@change=${(e) => updateQuantity(item.id, parseInt(e.target.value))}
/>
</div>
`)}
<div class="cart-totals">
<p>Subtotal: $${subtotal}</p>
<p>Tax (10%): $${tax}</p>
<p><strong>Total: $${total}</strong></p>
</div>
${() => cartItems().length > 0 ? html`
<button @click=${checkout}>Checkout</button>
<button @click=${clearCart}>Clear Cart</button>
` : html`
<p>Your cart is empty</p>
`}
</div>
`;
```
### Recent Searches History
```javascript
import { $, html } from 'sigpro';
// Persistent search history (max 10 items)
const searchHistory = $.storage('search-history', []);
// Add search to history
const addSearch = (query) => {
if (!query.trim()) return;
const current = searchHistory();
const newHistory = [
{ query, timestamp: new Date().toISOString() },
...current.filter(item => item.query !== query)
].slice(0, 10); // Keep only last 10
searchHistory(newHistory);
};
// Clear history
const clearHistory = () => {
searchHistory([]);
};
// Remove specific item
const removeFromHistory = (query) => {
searchHistory(searchHistory().filter(item => item.query !== query));
};
// Search component
const SearchWithHistory = () => {
const searchInput = $('');
const handleSearch = () => {
const query = searchInput();
if (query) {
addSearch(query);
performSearch(query);
searchInput('');
}
};
return html`
<div class="search-container">
<div class="search-box">
<input
type="search"
:value=${searchInput}
@keydown.enter=${handleSearch}
placeholder="Search..."
/>
<button @click=${handleSearch}>Search</button>
</div>
${() => searchHistory().length > 0 ? html`
<div class="search-history">
<h4>Recent Searches</h4>
${searchHistory().map(item => html`
<div class="history-item">
<button
class="history-query"
@click=${() => {
searchInput(item.query);
handleSearch();
}}
>
🔍 ${item.query}
</button>
<small>${new Date(item.timestamp).toLocaleString()}</small>
<button
class="remove-btn"
@click=${() => removeFromHistory(item.query)}
>
</button>
</div>
`)}
<button class="clear-btn" @click=${clearHistory}>
Clear History
</button>
</div>
` : ''}
</div>
`;
};
```
### Multiple Profiles / Accounts
```javascript
import { $, html } from 'sigpro';
// Profile manager
const profiles = $.storage('user-profiles', {
current: 'default',
list: {
default: {
name: 'Default',
theme: 'light',
preferences: {}
}
}
});
// Switch profile
const switchProfile = (profileId) => {
profiles({
...profiles(),
current: profileId
});
};
// Create profile
const createProfile = (name) => {
const id = `profile-${Date.now()}`;
profiles({
current: id,
list: {
...profiles().list,
[id]: {
name,
theme: 'light',
preferences: {},
createdAt: new Date().toISOString()
}
}
});
return id;
};
// Delete profile
const deleteProfile = (profileId) => {
if (profileId === 'default') return; // Can't delete default
const newList = { ...profiles().list };
delete newList[profileId];
profiles({
current: 'default',
list: newList
});
};
// Get current profile data
const currentProfile = $(() => {
const { current, list } = profiles();
return list[current] || list.default;
});
// Profile-aware settings
const profileTheme = $(() => currentProfile().theme);
const profilePreferences = $(() => currentProfile().preferences);
// Update profile data
const updateCurrentProfile = (updates) => {
const { current, list } = profiles();
profiles({
current,
list: {
...list,
[current]: {
...list[current],
...updates
}
}
});
};
// Profile selector component
const ProfileSelector = () => html`
<div class="profile-selector">
<select
:value=${() => profiles().current}
@change=${(e) => switchProfile(e.target.value)}
>
${Object.entries(profiles().list).map(([id, profile]) => html`
<option value="${id}">${profile.name}</option>
`)}
</select>
<button @click=${() => {
const name = prompt('Enter profile name:');
if (name) createProfile(name);
}}>
New Profile
</button>
</div>
`;
```
## 🛡️ Error Handling
### Storage Errors
```javascript
import { $ } from 'sigpro';
// Safe storage wrapper
const safeStorage = (key, initialValue, storage = localStorage) => {
try {
return $.storage(key, initialValue, storage);
} catch (error) {
console.warn(`Storage failed for ${key}, using in-memory fallback:`, error);
return $(initialValue);
}
};
// Usage with fallback
const theme = safeStorage('theme', 'light');
const user = safeStorage('user', null);
```
### Quota Exceeded Handling
```javascript
import { $ } from 'sigpro';
const createManagedStorage = (key, initialValue, maxSize = 1024 * 100) => { // 100KB limit
const signal = $.storage(key, initialValue);
// Monitor size
const size = $(0);
$.effect(() => {
try {
const value = signal();
const json = JSON.stringify(value);
const bytes = new Blob([json]).size;
size(bytes);
if (bytes > maxSize) {
console.warn(`Storage for ${key} exceeded ${maxSize} bytes`);
// Could implement cleanup strategy here
}
} catch (e) {
console.error('Size check failed:', e);
}
});
return { signal, size };
};
// Usage
const { signal: largeData, size } = createManagedStorage('app-data', {}, 50000);
```
## 📊 Storage Limits
| Storage Type | Typical Limit | Notes |
|--------------|---------------|-------|
| `localStorage` | 5-10MB | Varies by browser |
| `sessionStorage` | 5-10MB | Cleared when tab closes |
| `cookies` | 4KB | Not recommended for SigPro |
## 🎯 Best Practices
### 1. Validate Stored Data
```javascript
import { $ } from 'sigpro';
// Schema validation
const createValidatedStorage = (key, schema, defaultValue, storage) => {
const signal = $.storage(key, defaultValue, storage);
// Wrap to validate on read/write
const validated = (...args) => {
if (args.length) {
// Validate before writing
const value = args[0];
if (typeof value === 'function') {
// Handle functional updates
return validated(validated());
}
// Basic validation
const isValid = Object.keys(schema).every(key => {
const validator = schema[key];
return !validator || validator(value[key]);
});
if (!isValid) {
console.warn('Invalid data, skipping storage write');
return signal();
}
}
return signal(...args);
};
return validated;
};
// Usage
const userSchema = {
name: v => v && v.length > 0,
age: v => v >= 18 && v <= 120,
email: v => /@/.test(v)
};
const user = createValidatedStorage('user', userSchema, {
name: '',
age: 25,
email: ''
});
```
### 2. Handle Versioning
```javascript
import { $ } from 'sigpro';
const VERSION = 2;
const createVersionedStorage = (key, migrations, storage) => {
const raw = $.storage(key, { version: VERSION, data: {} }, storage);
const migrate = (data) => {
let current = data;
const currentVersion = current.version || 1;
for (let v = currentVersion; v < VERSION; v++) {
const migrator = migrations[v];
if (migrator) {
current = migrator(current);
}
}
return current;
};
// Migrate if needed
const stored = raw();
if (stored.version !== VERSION) {
const migrated = migrate(stored);
raw(migrated);
}
return raw;
};
// Usage
const migrations = {
1: (old) => ({
version: 2,
data: {
...old.data,
preferences: old.preferences || {}
}
})
};
const settings = createVersionedStorage('app-settings', migrations);
```
### 3. Encrypt Sensitive Data
```javascript
import { $ } from 'sigpro';
// Simple encryption (use proper crypto in production)
const encrypt = (text) => {
return btoa(text); // Base64 - NOT secure, just example
};
const decrypt = (text) => {
try {
return atob(text);
} catch {
return null;
}
};
const createSecureStorage = (key, initialValue, storage) => {
const encryptedKey = `enc_${key}`;
const signal = $.storage(encryptedKey, null, storage);
const secure = (...args) => {
if (args.length) {
// Encrypt before storing
const value = args[0];
const encrypted = encrypt(JSON.stringify(value));
return signal(encrypted);
}
// Decrypt when reading
const encrypted = signal();
if (!encrypted) return initialValue;
try {
const decrypted = decrypt(encrypted);
return decrypted ? JSON.parse(decrypted) : initialValue;
} catch {
return initialValue;
}
};
return secure;
};
// Usage
const secureToken = createSecureStorage('auth-token', null);
secureToken('sensitive-data-123'); // Stored encrypted
```
## 📈 Performance Considerations
| Operation | Cost | Notes |
|-----------|------|-------|
| Initial read | O(1) | Single storage read |
| Write | O(1) + JSON.stringify | Auto-save on change |
| Large objects | O(n) | Stringify/parse overhead |
| Multiple keys | O(k) | k = number of keys |
---
> **Pro Tip:** Use `sessionStorage` for temporary data like form drafts, and `localStorage` for persistent user preferences. Always validate data when reading from storage to handle corrupted values gracefully.

View File

@@ -0,0 +1,308 @@
# Getting Started with SigPro 🚀
Welcome to SigPro! This guide will help you get up and running with the library in minutes. SigPro is a minimalist reactive library that embraces the web platform - no compilation, no virtual DOM, just pure JavaScript and intelligent reactivity.
## 📦 Installation
Choose your preferred installation method:
```bash
# Using npm
npm install sigpro
# Using bun
bun add sigpro
# Or simply copy sigpro.js to your project
# (yes, it's that simple!)
```
## 🎯 Core Imports
```javascript
import { $, html } from 'sigpro';
```
That's it! Just two imports to unlock the entire reactive system:
- **`$`** - Creates reactive signals (the heart of reactivity)
- **`html`** - Template literal tag for reactive DOM rendering
## 🧠 Understanding the Basics
### Signals - The Reactive Heart
Signals are reactive values that automatically track dependencies and update when changed:
```javascript
// Create a signal with initial value
const count = $(0);
// Read value (with auto dependency tracking)
console.log(count()); // 0
// Set new value
count(5);
// Update using previous value
count(prev => prev + 1); // 6
// Create computed signals (auto-updating)
const firstName = $('John');
const lastName = $('Doe');
const fullName = $(() => `${firstName()} ${lastName()}`);
console.log(fullName()); // "John Doe"
firstName('Jane'); // fullName() now returns "Jane Doe"
```
### Effects - Automatic Reactions
Effects automatically run and re-run when their signal dependencies change:
```javascript
const count = $(0);
$.effect(() => {
console.log(`Count is: ${count()}`);
});
// Logs: "Count is: 0"
count(1);
// Logs: "Count is: 1"
// Effects can return cleanup functions
$.effect(() => {
const id = count();
const timer = setInterval(() => {
console.log(`Polling with count: ${id}`);
}, 1000);
// Cleanup runs before next effect execution
return () => clearInterval(timer);
});
```
### Rendering with `html`
The `html` tag creates reactive DOM fragments:
```javascript
const count = $(0);
const isActive = $(true);
const fragment = html`
<div class="counter">
<h2>Count: ${count}</h2>
<!-- Event binding -->
<button @click=${() => count(c => c + 1)}>
Increment
</button>
<!-- Boolean attributes -->
<button ?disabled=${() => !isActive()}>
Submit
</button>
</div>
`;
document.body.appendChild(fragment);
```
## 🎨 Your First Reactive App
Let's build a simple todo app to see SigPro in action:
```javascript
import { $, html } from 'sigpro';
// Create a simple todo app
function TodoApp() {
// Reactive state
const todos = $(['Learn SigPro', 'Build something awesome']);
const newTodo = $('');
// Computed value
const todoCount = $(() => todos().length);
// Add todo function
const addTodo = () => {
if (newTodo().trim()) {
todos([...todos(), newTodo()]);
newTodo('');
}
};
// Remove todo function
const removeTodo = (index) => {
todos(todos().filter((_, i) => i !== index));
};
// Return reactive template
return html`
<div style="max-width: 400px; margin: 2rem auto; font-family: system-ui;">
<h1>📝 Todo App</h1>
<!-- Input form -->
<div style="display: flex; gap: 8px; margin-bottom: 16px;">
<input
type="text"
:value=${newTodo}
placeholder="Add a new todo..."
style="flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px;"
@keydown.enter=${addTodo}
/>
<button
@click=${addTodo}
style="padding: 8px 16px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;"
>
Add
</button>
</div>
<!-- Todo count -->
<p>Total todos: ${todoCount}</p>
<!-- Todo list -->
<ul style="list-style: none; padding: 0;">
${() => todos().map((todo, index) => html`
<li style="display: flex; justify-content: space-between; align-items: center; padding: 8px; margin: 4px 0; background: #f5f5f5; border-radius: 4px;">
<span>${todo}</span>
<button
@click=${() => removeTodo(index)}
style="padding: 4px 8px; background: #ff4444; color: white; border: none; border-radius: 4px; cursor: pointer;"
>
</button>
</li>
`)}
</ul>
</div>
`;
}
// Mount the app
document.body.appendChild(TodoApp());
```
## 🎯 Key Concepts
### 1. **Signal Patterns**
| Pattern | Example | Use Case |
|---------|---------|----------|
| Basic signal | `const count = $(0)` | Simple values |
| Computed | `$( () => first() + last() )` | Derived values |
| Signal update | `count(5)` | Direct set |
| Functional update | `count(prev => prev + 1)` | Based on previous |
### 2. **Effect Patterns**
```javascript
// Basic effect
$.effect(() => console.log(count()));
// Effect with cleanup
$.effect(() => {
const timer = setInterval(() => {}, 1000);
return () => clearInterval(timer);
});
// Stopping an effect
const stop = $.effect(() => {});
stop(); // Effect won't run again
```
### 3. **HTML Directives**
| Directive | Example | Description |
|-----------|---------|-------------|
| `@event` | `@click=${handler}` | Event listeners |
| `:property` | `:value=${signal}` | Two-way binding |
| `?attribute` | `?disabled=${signal}` | Boolean attributes |
| `.property` | `.scrollTop=${value}` | DOM properties |
## 💡 Pro Tips for Beginners
### 1. **Start Simple**
```javascript
// Begin with basic signals
const name = $('World');
html`<h1>Hello, ${name}!</h1>`;
```
### 2. **Use Computed Signals for Derived State**
```javascript
// ❌ Don't compute in template
html`<p>Total: ${items().length * price()}</p>`;
// ✅ Compute with signals
const total = $(() => items().length * price());
html`<p>Total: ${total}</p>`;
```
### 3. **Leverage Effects for Side Effects**
```javascript
// Auto-save to localStorage
$.effect(() => {
localStorage.setItem('draft', JSON.stringify(draft()));
});
```
## 🔧 VS Code Setup
For the best development experience, install these VS Code extensions:
- **lit-html** - Adds syntax highlighting for `html` tagged templates
- **Prettier** - Automatically formats your template literals
```javascript
// With lit-html extension, you get full syntax highlighting!
html`
<div style="color: #ff4444; background: linear-gradient(45deg, blue, green)">
<h1>Beautiful highlighted template</h1>
</div>
`
```
## 📁 Project Structure
Here's a recommended structure for larger apps:
```
my-sigpro-app/
├── index.html
├── main.js
├── components/
│ ├── Button.js
│ ├── TodoList.js
│ └── TodoItem.js
├── pages/
│ ├── HomePage.js
│ └── AboutPage.js
└── utils/
└── helpers.js
```
Example `main.js`:
```javascript
import { $, html } from 'sigpro';
import HomePage from './pages/HomePage.js';
// Mount your app
document.body.appendChild(HomePage());
```
## 🎓 Summary
You've learned:
- ✅ How to install SigPro
- ✅ Core concepts: signals, effects, and reactive rendering
- ✅ Built a complete todo app
- ✅ Key patterns and best practices
- ✅ How to structure larger applications
**Remember:** SigPro embraces the web platform. You're writing vanilla JavaScript with superpowers—no compilation, no lock-in, just clean, maintainable code that will work for years to come.
> "Stop fighting the platform. Start building with it."
Happy coding! 🎉

135
packages/docs/guide/why.md Normal file
View File

@@ -0,0 +1,135 @@
# Why SigPro? ❓
After years of building applications with React, Vue, and Svelte—investing countless hours mastering their unique mental models, build tools, and update cycles—I kept circling back to the same realization: no matter how sophisticated the framework, it all eventually compiles down to HTML, CSS, and vanilla JavaScript. The web platform has evolved tremendously, yet many libraries continue to reinvent the wheel, creating parallel universes with their own rules, their own syntaxes, and their own steep learning curves.
**SigPro is my answer to a simple question:** Why fight the platform when we can embrace it?
## 🌐 The Web Platform Is Finally Ready
Modern browsers now offer powerful primitives that make true reactivity possible without virtual DOM diffing, without compilers, and without lock-in:
| Browser Primitive | What It Enables |
|-------------------|-----------------|
| **Custom Elements** | Create reusable components with native browser APIs |
| **Shadow DOM** | Encapsulate styles and markup without preprocessors |
| **CSS Custom Properties** | Dynamic theming without CSS-in-JS |
| **Microtask Queues** | Efficient update batching without complex scheduling |
## 🎯 The SigPro Philosophy
SigPro strips away the complexity, delivering a reactive programming model that feels familiar but stays remarkably close to vanilla JS:
- **No JSX transformations** - Just template literals
- **No template compilers** - The browser parses your HTML
- **No proprietary syntax to learn** - Just functions and signals
- **No build step required** - Works directly in the browser
```javascript
// Just vanilla JavaScript with signals
import { $, html } from 'sigpro';
const count = $(0);
document.body.appendChild(html`
<div>
<p>Count: ${count}</p>
<button @click=${() => count(c => c + 1)}>
Increment
</button>
</div>
`);
```
## 📊 Comparative
| Metric | SigPro | Solid | Svelte | Vue | React |
|--------|--------|-------|--------|-----|-------|
| **Bundle Size** (gzip) | 🥇 **5.2KB** | 🥈 15KB | 🥉 16.6KB | 20.4KB | 43.9KB |
| **Time to Interactive** | 🥇 **0.8s** | 🥈 1.3s | 🥉 1.4s | 1.6s | 2.3s |
| **Initial Render** (ms) | 🥇 **124ms** | 🥈 198ms | 🥉 287ms | 298ms | 452ms |
| **Update Performance** (ms) | 🥇 **4ms** | 🥈 5ms | 🥈 5ms | 🥉 7ms | 18ms |
| **Dependencies** | 🥇 **0** | 🥇 **0** | 🥇 **0** | 🥈 2 | 🥉 5 |
| **Compilation Required** | 🥇 **No** | 🥇 **No** | 🥈 Yes | 🥇 **No** | 🥇 **No** |
| **Browser Native** | 🥇 **Yes** | 🥈 Partial | 🥉 Partial | 🥉 Partial | No |
| **Framework Lock-in** | 🥇 **None** | 🥈 Medium | 🥉 High | 🥈 Medium | 🥉 High |
| **Longevity** (standards-based) | 🥇 **10+ years** | 🥈 5 years | 🥉 3 years | 🥈 5 years | 🥈 5 years |
## 🔑 Core Principles
SigPro is built on four fundamental principles:
### 📡 **True Reactivity**
Automatic dependency tracking with no manual subscriptions. When a signal changes, only the exact DOM nodes that depend on it update—surgically, efficiently, instantly.
### ⚡ **Surgical Updates**
No virtual DOM diffing. No tree reconciliation. Just direct DOM updates where and when needed. The result is predictable performance that scales with your content, not your component count.
### 🧩 **Web Standards**
Built on Custom Elements, not a custom rendering engine. Your components are real web components that work in any framework—or none at all.
### 🔬 **Predictable**
No magic, just signals and effects. What you see is what you get. The debugging experience is straightforward because there's no framework layer between your code and the browser.
## 🎨 The Development Experience
```javascript
// With VS Code + lit-html extension, you get:
// ✅ Syntax highlighting
// ✅ Color previews
// ✅ Auto-formatting
// ✅ IntelliSense
html`
<div style="color: #ff4444; background: linear-gradient(45deg, blue, green)">
<h1>Beautiful highlighted template</h1>
</div>
`
```
## ⏱️ Built for the Long Term
What emerged is a library that proves we've reached a turning point: the web is finally mature enough that we don't need to abstract it anymore. We can build reactive, component-based applications using virtually pure JavaScript, leveraging the platform's latest advances instead of working against them.
**The result isn't just smaller bundles or faster rendering—it's code that will still run 10 years from now, in any browser, without maintenance.**
## 📈 The Verdict
While other frameworks build parallel universes with proprietary syntax and compilation steps, SigPro embraces the web platform. SigPro isn't just another framework—it's a return to fundamentals, showing that the dream of simple, powerful reactivity is now achievable with the tools browsers give us out of the box.
> *"Stop fighting the platform. Start building with it."*
## 🚀 Ready to Start?
[Get Started with SigPro](/guide/getting-started) • [View on GitHub](https://github.com/natxocc/sigpro) • [npm Package](https://www.npmjs.com/package/sigpro)
<style>
table {
width: 100%;
border-collapse: collapse;
margin: 2rem 0;
}
th {
background-color: var(--vp-c-bg-soft);
padding: 0.75rem;
text-align: left;
}
td {
padding: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
tr:hover {
background-color: var(--vp-c-bg-soft);
}
blockquote {
margin: 2rem 0;
padding: 1.5rem;
background: linear-gradient(135deg, var(--vp-c-brand-soft) 0%, transparent 100%);
border-radius: 12px;
font-size: 1.2rem;
font-style: italic;
}
</style>

84
packages/docs/index.md Normal file
View File

@@ -0,0 +1,84 @@
---
# https://vitepress.dev/reference/default-theme-home-page
layout: home
hero:
name: "SigPro"
text: "Reactivity for the Web Platform"
tagline: A minimalist reactive library for building web interfaces with signals, effects, and native web components. No compilation, no virtual DOM, just pure JavaScript and intelligent reactivity.
image:
src: /logo.svg
alt: SigPro
actions:
- theme: brand
text: Get Started
link: /guide/getting-started
features:
- title: ⚡ 3KB gzipped
details: Minimal footprint with maximum impact. No heavy dependencies, just pure reactivity.
- title: 🎯 Native Web Components
details: Built on Custom Elements and Shadow DOM. Leverage the platform, don't fight it.
- title: 🔄 Signal-based Reactivity
details: Fine-grained updates without virtual DOM diffing. Just intelligent, automatic reactivity.
---
<div class="custom-container">
<p class="npm-stats">
<img src="https://badge.fury.io/js/sigpro.svg" alt="npm version">
<img src="https://img.shields.io/bundlephobia/minzip/sigpro" alt="bundle size">
<img src="https://img.shields.io/npm/l/sigpro" alt="license">
</p>
</div>
<div class="verdict-quote">
<p><strong>"Stop fighting the platform. Start building with it."</strong></p>
</div>
<style>
.npm-stats {
text-align: center;
margin: 2rem 0;
}
.npm-stats img {
margin: 0 0.5rem;
display: inline-block;
}
.custom-container {
max-width: 1152px;
margin: 0 auto;
padding: 0 24px;
}
.verdict-quote {
text-align: center;
font-size: 1.5rem;
margin: 3rem 0;
padding: 2rem;
background: linear-gradient(135deg, var(--vp-c-brand-soft) 0%, transparent 100%);
border-radius: 12px;
}
table {
width: 100%;
border-collapse: collapse;
margin: 2rem 0;
}
th {
background-color: var(--vp-c-bg-soft);
padding: 0.75rem;
text-align: left;
}
td {
padding: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
tr:hover {
background-color: var(--vp-c-bg-soft);
}
</style>

118
packages/docs/logo.svg Normal file
View File

@@ -0,0 +1,118 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="600.000000pt" height="591.000000pt" viewBox="0 0 600.000000 591.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,591.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M1130 3454 c-44 -9 -84 -27 -123 -57 -97 -76 -91 -242 12 -310 34
-23 60 -32 193 -71 58 -17 78 -36 78 -74 0 -28 -24 -44 -74 -49 -65 -8 -137
20 -181 68 l-36 40 -27 -33 c-15 -18 -37 -43 -50 -56 -26 -27 -20 -40 35 -86
65 -55 118 -71 233 -71 89 1 112 4 152 24 94 46 137 146 108 252 -20 71 -81
112 -224 152 -124 35 -150 64 -101 112 42 43 147 25 203 -35 18 -20 19 -19 70
30 47 45 51 53 40 69 -22 31 -71 67 -108 80 -39 13 -161 22 -200 15z m193 -36
c56 -23 97 -54 97 -73 0 -7 -17 -27 -37 -44 -36 -30 -37 -31 -57 -13 -12 10
-34 24 -51 31 -34 14 -148 24 -140 11 2 -4 -4 -10 -15 -13 -32 -8 -43 -52 -24
-93 9 -19 19 -32 22 -30 2 3 17 -1 33 -9 15 -8 39 -15 54 -15 14 0 25 -5 25
-11 0 -6 9 -8 20 -4 11 4 20 2 20 -4 0 -6 9 -11 21 -11 37 0 114 -59 133 -103
24 -55 15 -138 -18 -182 -28 -37 -101 -79 -123 -71 -9 4 -11 1 -7 -5 5 -8 -22
-10 -94 -7 -85 3 -106 7 -131 26 -17 12 -37 22 -46 22 -8 0 -15 3 -15 8 0 4
-10 12 -22 17 -41 19 -44 40 -12 77 17 19 37 32 45 30 27 -9 69 -44 64 -53 -4
-5 2 -6 11 -2 11 4 15 3 11 -4 -5 -7 1 -9 15 -5 12 3 19 1 15 -4 -3 -6 25 -11
68 -13 69 -2 77 0 100 23 14 14 31 26 38 27 9 0 9 2 0 6 -7 2 -13 16 -13 29 0
33 -38 66 -91 81 -24 6 -71 18 -104 27 -32 9 -63 23 -68 31 -4 8 -12 13 -17
10 -4 -3 -23 13 -40 36 -28 34 -33 48 -33 97 0 60 35 138 56 125 5 -3 7 -1 3
5 -8 13 2 20 69 49 68 30 168 31 238 1z"/>
<path d="M1031 3144 c0 -11 3 -14 6 -6 3 7 2 16 -1 19 -3 4 -6 -2 -5 -13z"/>
<path d="M945 2900 c-6 -9 11 -50 21 -50 2 0 0 9 -6 20 -6 11 -8 25 -5 30 3 6
4 10 1 10 -3 0 -8 -4 -11 -10z"/>
<path d="M2428 3445 c-154 -39 -259 -173 -261 -333 -2 -111 21 -177 86 -248
77 -85 134 -108 267 -108 86 -1 113 3 151 20 114 53 169 139 177 276 l5 88
-167 0 -166 0 0 -66 0 -65 85 3 c74 3 85 1 85 -14 0 -27 -48 -76 -87 -89 -49
-16 -133 -6 -176 22 -40 27 -82 89 -91 137 -8 44 10 119 37 159 64 94 220 110
304 31 l23 -22 56 56 56 56 -43 32 c-44 32 -97 59 -131 65 -65 13 -159 13
-210 0z m202 -16 c8 -2 17 -7 20 -10 3 -3 21 -10 40 -15 19 -5 35 -15 35 -21
0 -6 4 -15 9 -20 5 -5 6 -3 2 5 -4 6 -3 12 3 12 6 0 15 -9 22 -21 9 -17 7 -24
-10 -40 -11 -10 -24 -27 -30 -36 -9 -17 -13 -16 -59 9 -62 34 -139 53 -157 38
-8 -6 -29 -12 -48 -13 -19 -1 -33 -5 -30 -9 2 -5 -3 -8 -12 -8 -9 0 -14 -4
-10 -9 3 -5 -4 -12 -15 -16 -11 -3 -20 -13 -20 -22 0 -14 -2 -14 -19 1 -13 12
-21 14 -26 6 -3 -6 -1 -9 6 -8 6 2 14 -3 17 -11 2 -8 0 -11 -6 -7 -6 4 -8 -1
-5 -13 3 -10 0 -22 -6 -25 -13 -9 -15 -129 -2 -173 12 -42 76 -107 125 -127
78 -32 198 -9 226 43 5 11 17 27 26 35 10 11 11 16 3 16 -7 0 -9 3 -6 7 4 3 2
12 -4 20 -8 8 -35 13 -79 13 l-68 0 -3 44 -4 44 124 1 c69 1 129 5 134 8 6 4
7 1 2 -7 -6 -9 -4 -12 6 -8 10 4 14 -4 14 -31 0 -20 -4 -42 -9 -50 -6 -10 -6
-12 3 -7 8 6 10 -4 5 -36 -7 -45 -72 -142 -110 -162 -10 -6 -21 -14 -24 -17
-21 -27 -213 -47 -265 -27 -75 28 -115 50 -110 58 3 6 0 7 -8 4 -14 -5 -87 72
-87 92 0 5 5 2 10 -6 7 -12 9 -10 8 7 -1 11 -7 19 -14 16 -12 -4 -27 34 -28
74 -1 12 -4 25 -8 29 -12 11 -10 64 2 64 6 0 10 5 10 11 0 5 -5 7 -11 3 -8 -5
-8 -1 0 14 6 11 11 34 11 51 0 17 4 31 9 31 8 0 24 35 23 50 0 3 19 28 44 55
25 29 42 41 38 30 l-6 -20 13 20 c6 11 13 21 15 23 1 2 32 13 68 26 62 20 174
28 226 15z"/>
<path d="M4670 3451 c-19 -4 -56 -18 -82 -31 -145 -72 -217 -236 -178 -409 14
-64 64 -147 106 -179 16 -12 34 -26 41 -32 6 -5 39 -18 72 -30 114 -38 243
-22 338 44 205 141 184 489 -37 606 -64 33 -178 47 -260 31z m177 -22 c23 -6
40 -16 37 -21 -4 -6 2 -7 15 -3 14 4 23 2 27 -9 3 -9 12 -12 21 -9 11 4 14 2
9 -5 -4 -7 -1 -12 8 -12 21 0 85 -89 86 -117 0 -13 4 -23 8 -23 16 0 30 -74
26 -130 -7 -100 -11 -119 -24 -135 -7 -8 -9 -15 -6 -15 10 0 -52 -85 -78 -107
-20 -16 -50 -32 -116 -61 -31 -14 -161 -10 -219 7 -24 7 -61 23 -81 36 -48 30
-114 110 -104 126 5 8 3 9 -6 4 -10 -6 -12 -3 -8 13 4 12 2 22 -2 22 -4 0 -11
27 -15 60 -8 71 2 143 18 134 8 -5 7 -2 0 7 -16 16 -17 33 -1 23 6 -4 8 -3 5
3 -10 16 13 74 26 66 6 -3 7 -1 3 6 -9 14 57 82 106 108 73 40 189 54 265 32z"/>
<path d="M4676 3307 c-22 -10 -47 -28 -57 -40 -10 -12 -18 -18 -19 -14 0 4 -6
2 -14 -5 -7 -7 -11 -19 -8 -27 3 -8 1 -12 -5 -8 -11 7 -16 -28 -16 -113 0 -74
6 -109 17 -102 5 3 7 -2 3 -11 -4 -10 -2 -16 6 -14 7 1 11 -4 9 -11 -1 -8 2
-11 8 -7 5 3 10 -1 10 -10 0 -8 16 -23 35 -32 19 -9 33 -19 31 -22 -2 -3 27
-6 64 -6 37 0 66 4 63 8 -3 5 1 7 8 6 23 -4 65 18 87 45 11 14 25 26 32 26 6
0 10 6 8 12 -3 7 1 18 7 25 15 15 16 195 2 186 -5 -3 -8 0 -5 7 5 15 -62 84
-102 105 -45 23 -118 24 -164 2z m164 -26 c19 -10 49 -37 65 -60 27 -39 30
-50 30 -119 0 -65 -4 -83 -25 -114 -36 -53 -85 -78 -156 -78 -68 0 -107 20
-149 74 -40 52 -46 146 -15 210 49 102 149 137 250 87z"/>
<path d="M1619 3443 c0 -2 0 -33 -1 -70 l0 -68 58 1 59 0 -2 -205 -2 -205 -58
3 -58 2 0 -70 0 -71 203 0 202 0 0 70 0 70 -77 -1 c-43 -1 -68 -4 -55 -6 12
-2 22 -7 22 -11 0 -4 19 -7 42 -7 l43 0 3 -48 3 -48 -165 3 c-92 2 -166 -1
-166 -5 0 -4 -9 -3 -21 3 -16 9 -19 19 -17 53 l3 41 47 1 c76 2 76 -1 76 223
1 109 -3 203 -7 211 -5 8 -29 14 -57 15 -32 1 -49 5 -48 14 1 6 2 28 3 47 l1
35 176 0 176 0 -4 -47 -3 -48 -42 -1 c-73 -1 -73 -2 -72 -220 1 -107 5 -194
10 -194 5 0 9 90 9 199 l0 199 60 -5 61 -6 -3 72 -3 72 -198 2 c-108 1 -197 1
-198 0z"/>
<path d="M3018 3102 l-3 -342 85 0 85 0 0 112 0 113 105 5 c113 6 145 17 192
67 57 60 76 188 39 261 -26 51 -77 99 -118 113 -19 6 -112 12 -208 12 l-175 2
-2 -343z m375 307 c20 -5 37 -14 37 -19 0 -6 6 -10 14 -10 8 0 18 -5 22 -12 5
-7 3 -8 -6 -3 -9 5 -11 4 -6 -3 4 -7 13 -12 19 -12 7 0 20 -20 30 -45 23 -54
19 -135 -8 -197 -4 -10 -11 -15 -16 -12 -5 3 -6 -2 -3 -10 4 -10 -10 -24 -43
-43 -53 -31 -55 -32 -167 -38 -107 -5 -112 -12 -106 -131 4 -83 3 -94 -12 -98
-10 -2 -18 -1 -18 3 0 3 -20 5 -45 3 l-45 -4 0 321 0 321 158 0 c86 0 174 -5
195 -11z"/>
<path d="M3187 3308 c-27 -20 -34 -173 -9 -183 26 -10 151 -11 144 -1 -3 5 5
7 16 4 12 -4 20 -3 18 1 -3 4 4 15 15 24 29 25 26 100 -5 134 -20 21 -35 26
-92 30 -50 3 -74 1 -87 -9z m157 -29 c32 -25 36 -84 7 -120 -16 -20 -30 -24
-93 -27 l-74 -4 0 86 1 86 66 0 c52 0 72 -4 93 -21z"/>
<path d="M3710 3103 l0 -343 83 0 82 0 -3 120 -3 120 45 0 c44 0 45 -1 79 -57
19 -32 49 -86 67 -120 l32 -62 95 -3 c52 -2 97 -1 99 1 7 7 -18 31 -27 25 -5
-3 -7 1 -4 8 3 7 -22 59 -55 116 -60 103 -68 122 -51 122 16 0 86 80 98 112
39 108 -9 228 -110 277 -37 18 -65 21 -235 23 l-192 3 0 -342z m385 313 c26
-5 74 -37 71 -49 -1 -6 -1 -9 1 -4 18 31 73 -77 73 -143 0 -56 -36 -127 -78
-155 -17 -11 -35 -30 -41 -42 -9 -19 -6 -27 14 -47 14 -14 25 -32 25 -41 0 -8
5 -15 11 -15 6 0 8 -9 4 -20 -4 -13 -2 -19 4 -18 12 3 68 -88 60 -96 -4 -4
-77 -7 -120 -5 -17 0 -54 64 -52 87 0 7 -1 11 -5 8 -6 -7 -32 41 -32 61 0 8
-4 12 -8 9 -4 -3 -17 11 -27 31 -21 38 -55 58 -67 39 -6 -8 -10 -8 -16 1 -6 9
-13 9 -32 0 -27 -14 -27 -12 -29 -150 l-1 -88 -57 2 -58 2 3 316 c2 235 6 317
15 323 10 6 301 1 342 -6z"/>
<path d="M3886 3328 c3 -4 -1 -8 -9 -8 -22 0 -37 -128 -21 -187 6 -22 11 -23
101 -23 91 0 96 1 124 29 41 41 43 113 5 146 -51 44 -67 55 -79 55 -8 0 -7 -4
3 -10 11 -7 -4 -10 -48 -10 -35 0 -61 4 -58 8 3 5 -1 9 -9 9 -8 0 -12 -4 -9
-9z m180 -49 c43 -43 31 -133 -19 -143 -12 -3 -57 -7 -99 -8 l-78 -3 0 93 0
94 85 -3 c75 -4 87 -7 111 -30z"/>
<path d="M4167 3099 c7 -7 15 -10 18 -7 3 3 -2 9 -12 12 -14 6 -15 5 -6 -5z"/>
<path d="M3986 3017 c3 -10 9 -15 12 -12 3 3 0 11 -7 18 -10 9 -11 8 -5 -6z"/>
<path d="M1643 2830 c0 -25 2 -35 4 -22 2 12 2 32 0 45 -2 12 -4 2 -4 -23z"/>
<path d="M2923 2373 c13 -4 17 -15 17 -59 0 -60 -16 -77 -51 -55 -11 6 -19 7
-19 2 0 -6 6 -13 13 -18 45 -28 81 10 75 82 -2 33 1 46 12 49 8 2 -3 4 -25 4
-22 0 -32 -2 -22 -5z"/>
<path d="M3072 2368 c-24 -24 -13 -49 28 -65 27 -11 40 -22 38 -32 -4 -20 -44
-27 -64 -10 -8 6 -17 9 -20 6 -3 -3 5 -12 18 -21 37 -24 87 -7 88 30 0 15 -19
29 -55 41 -34 11 -39 30 -13 47 13 8 23 8 38 -2 27 -16 35 -15 20 3 -15 18
-61 20 -78 3z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@@ -0,0 +1,118 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="600.000000pt" height="591.000000pt" viewBox="0 0 600.000000 591.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,591.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M1130 3454 c-44 -9 -84 -27 -123 -57 -97 -76 -91 -242 12 -310 34
-23 60 -32 193 -71 58 -17 78 -36 78 -74 0 -28 -24 -44 -74 -49 -65 -8 -137
20 -181 68 l-36 40 -27 -33 c-15 -18 -37 -43 -50 -56 -26 -27 -20 -40 35 -86
65 -55 118 -71 233 -71 89 1 112 4 152 24 94 46 137 146 108 252 -20 71 -81
112 -224 152 -124 35 -150 64 -101 112 42 43 147 25 203 -35 18 -20 19 -19 70
30 47 45 51 53 40 69 -22 31 -71 67 -108 80 -39 13 -161 22 -200 15z m193 -36
c56 -23 97 -54 97 -73 0 -7 -17 -27 -37 -44 -36 -30 -37 -31 -57 -13 -12 10
-34 24 -51 31 -34 14 -148 24 -140 11 2 -4 -4 -10 -15 -13 -32 -8 -43 -52 -24
-93 9 -19 19 -32 22 -30 2 3 17 -1 33 -9 15 -8 39 -15 54 -15 14 0 25 -5 25
-11 0 -6 9 -8 20 -4 11 4 20 2 20 -4 0 -6 9 -11 21 -11 37 0 114 -59 133 -103
24 -55 15 -138 -18 -182 -28 -37 -101 -79 -123 -71 -9 4 -11 1 -7 -5 5 -8 -22
-10 -94 -7 -85 3 -106 7 -131 26 -17 12 -37 22 -46 22 -8 0 -15 3 -15 8 0 4
-10 12 -22 17 -41 19 -44 40 -12 77 17 19 37 32 45 30 27 -9 69 -44 64 -53 -4
-5 2 -6 11 -2 11 4 15 3 11 -4 -5 -7 1 -9 15 -5 12 3 19 1 15 -4 -3 -6 25 -11
68 -13 69 -2 77 0 100 23 14 14 31 26 38 27 9 0 9 2 0 6 -7 2 -13 16 -13 29 0
33 -38 66 -91 81 -24 6 -71 18 -104 27 -32 9 -63 23 -68 31 -4 8 -12 13 -17
10 -4 -3 -23 13 -40 36 -28 34 -33 48 -33 97 0 60 35 138 56 125 5 -3 7 -1 3
5 -8 13 2 20 69 49 68 30 168 31 238 1z"/>
<path d="M1031 3144 c0 -11 3 -14 6 -6 3 7 2 16 -1 19 -3 4 -6 -2 -5 -13z"/>
<path d="M945 2900 c-6 -9 11 -50 21 -50 2 0 0 9 -6 20 -6 11 -8 25 -5 30 3 6
4 10 1 10 -3 0 -8 -4 -11 -10z"/>
<path d="M2428 3445 c-154 -39 -259 -173 -261 -333 -2 -111 21 -177 86 -248
77 -85 134 -108 267 -108 86 -1 113 3 151 20 114 53 169 139 177 276 l5 88
-167 0 -166 0 0 -66 0 -65 85 3 c74 3 85 1 85 -14 0 -27 -48 -76 -87 -89 -49
-16 -133 -6 -176 22 -40 27 -82 89 -91 137 -8 44 10 119 37 159 64 94 220 110
304 31 l23 -22 56 56 56 56 -43 32 c-44 32 -97 59 -131 65 -65 13 -159 13
-210 0z m202 -16 c8 -2 17 -7 20 -10 3 -3 21 -10 40 -15 19 -5 35 -15 35 -21
0 -6 4 -15 9 -20 5 -5 6 -3 2 5 -4 6 -3 12 3 12 6 0 15 -9 22 -21 9 -17 7 -24
-10 -40 -11 -10 -24 -27 -30 -36 -9 -17 -13 -16 -59 9 -62 34 -139 53 -157 38
-8 -6 -29 -12 -48 -13 -19 -1 -33 -5 -30 -9 2 -5 -3 -8 -12 -8 -9 0 -14 -4
-10 -9 3 -5 -4 -12 -15 -16 -11 -3 -20 -13 -20 -22 0 -14 -2 -14 -19 1 -13 12
-21 14 -26 6 -3 -6 -1 -9 6 -8 6 2 14 -3 17 -11 2 -8 0 -11 -6 -7 -6 4 -8 -1
-5 -13 3 -10 0 -22 -6 -25 -13 -9 -15 -129 -2 -173 12 -42 76 -107 125 -127
78 -32 198 -9 226 43 5 11 17 27 26 35 10 11 11 16 3 16 -7 0 -9 3 -6 7 4 3 2
12 -4 20 -8 8 -35 13 -79 13 l-68 0 -3 44 -4 44 124 1 c69 1 129 5 134 8 6 4
7 1 2 -7 -6 -9 -4 -12 6 -8 10 4 14 -4 14 -31 0 -20 -4 -42 -9 -50 -6 -10 -6
-12 3 -7 8 6 10 -4 5 -36 -7 -45 -72 -142 -110 -162 -10 -6 -21 -14 -24 -17
-21 -27 -213 -47 -265 -27 -75 28 -115 50 -110 58 3 6 0 7 -8 4 -14 -5 -87 72
-87 92 0 5 5 2 10 -6 7 -12 9 -10 8 7 -1 11 -7 19 -14 16 -12 -4 -27 34 -28
74 -1 12 -4 25 -8 29 -12 11 -10 64 2 64 6 0 10 5 10 11 0 5 -5 7 -11 3 -8 -5
-8 -1 0 14 6 11 11 34 11 51 0 17 4 31 9 31 8 0 24 35 23 50 0 3 19 28 44 55
25 29 42 41 38 30 l-6 -20 13 20 c6 11 13 21 15 23 1 2 32 13 68 26 62 20 174
28 226 15z"/>
<path d="M4670 3451 c-19 -4 -56 -18 -82 -31 -145 -72 -217 -236 -178 -409 14
-64 64 -147 106 -179 16 -12 34 -26 41 -32 6 -5 39 -18 72 -30 114 -38 243
-22 338 44 205 141 184 489 -37 606 -64 33 -178 47 -260 31z m177 -22 c23 -6
40 -16 37 -21 -4 -6 2 -7 15 -3 14 4 23 2 27 -9 3 -9 12 -12 21 -9 11 4 14 2
9 -5 -4 -7 -1 -12 8 -12 21 0 85 -89 86 -117 0 -13 4 -23 8 -23 16 0 30 -74
26 -130 -7 -100 -11 -119 -24 -135 -7 -8 -9 -15 -6 -15 10 0 -52 -85 -78 -107
-20 -16 -50 -32 -116 -61 -31 -14 -161 -10 -219 7 -24 7 -61 23 -81 36 -48 30
-114 110 -104 126 5 8 3 9 -6 4 -10 -6 -12 -3 -8 13 4 12 2 22 -2 22 -4 0 -11
27 -15 60 -8 71 2 143 18 134 8 -5 7 -2 0 7 -16 16 -17 33 -1 23 6 -4 8 -3 5
3 -10 16 13 74 26 66 6 -3 7 -1 3 6 -9 14 57 82 106 108 73 40 189 54 265 32z"/>
<path d="M4676 3307 c-22 -10 -47 -28 -57 -40 -10 -12 -18 -18 -19 -14 0 4 -6
2 -14 -5 -7 -7 -11 -19 -8 -27 3 -8 1 -12 -5 -8 -11 7 -16 -28 -16 -113 0 -74
6 -109 17 -102 5 3 7 -2 3 -11 -4 -10 -2 -16 6 -14 7 1 11 -4 9 -11 -1 -8 2
-11 8 -7 5 3 10 -1 10 -10 0 -8 16 -23 35 -32 19 -9 33 -19 31 -22 -2 -3 27
-6 64 -6 37 0 66 4 63 8 -3 5 1 7 8 6 23 -4 65 18 87 45 11 14 25 26 32 26 6
0 10 6 8 12 -3 7 1 18 7 25 15 15 16 195 2 186 -5 -3 -8 0 -5 7 5 15 -62 84
-102 105 -45 23 -118 24 -164 2z m164 -26 c19 -10 49 -37 65 -60 27 -39 30
-50 30 -119 0 -65 -4 -83 -25 -114 -36 -53 -85 -78 -156 -78 -68 0 -107 20
-149 74 -40 52 -46 146 -15 210 49 102 149 137 250 87z"/>
<path d="M1619 3443 c0 -2 0 -33 -1 -70 l0 -68 58 1 59 0 -2 -205 -2 -205 -58
3 -58 2 0 -70 0 -71 203 0 202 0 0 70 0 70 -77 -1 c-43 -1 -68 -4 -55 -6 12
-2 22 -7 22 -11 0 -4 19 -7 42 -7 l43 0 3 -48 3 -48 -165 3 c-92 2 -166 -1
-166 -5 0 -4 -9 -3 -21 3 -16 9 -19 19 -17 53 l3 41 47 1 c76 2 76 -1 76 223
1 109 -3 203 -7 211 -5 8 -29 14 -57 15 -32 1 -49 5 -48 14 1 6 2 28 3 47 l1
35 176 0 176 0 -4 -47 -3 -48 -42 -1 c-73 -1 -73 -2 -72 -220 1 -107 5 -194
10 -194 5 0 9 90 9 199 l0 199 60 -5 61 -6 -3 72 -3 72 -198 2 c-108 1 -197 1
-198 0z"/>
<path d="M3018 3102 l-3 -342 85 0 85 0 0 112 0 113 105 5 c113 6 145 17 192
67 57 60 76 188 39 261 -26 51 -77 99 -118 113 -19 6 -112 12 -208 12 l-175 2
-2 -343z m375 307 c20 -5 37 -14 37 -19 0 -6 6 -10 14 -10 8 0 18 -5 22 -12 5
-7 3 -8 -6 -3 -9 5 -11 4 -6 -3 4 -7 13 -12 19 -12 7 0 20 -20 30 -45 23 -54
19 -135 -8 -197 -4 -10 -11 -15 -16 -12 -5 3 -6 -2 -3 -10 4 -10 -10 -24 -43
-43 -53 -31 -55 -32 -167 -38 -107 -5 -112 -12 -106 -131 4 -83 3 -94 -12 -98
-10 -2 -18 -1 -18 3 0 3 -20 5 -45 3 l-45 -4 0 321 0 321 158 0 c86 0 174 -5
195 -11z"/>
<path d="M3187 3308 c-27 -20 -34 -173 -9 -183 26 -10 151 -11 144 -1 -3 5 5
7 16 4 12 -4 20 -3 18 1 -3 4 4 15 15 24 29 25 26 100 -5 134 -20 21 -35 26
-92 30 -50 3 -74 1 -87 -9z m157 -29 c32 -25 36 -84 7 -120 -16 -20 -30 -24
-93 -27 l-74 -4 0 86 1 86 66 0 c52 0 72 -4 93 -21z"/>
<path d="M3710 3103 l0 -343 83 0 82 0 -3 120 -3 120 45 0 c44 0 45 -1 79 -57
19 -32 49 -86 67 -120 l32 -62 95 -3 c52 -2 97 -1 99 1 7 7 -18 31 -27 25 -5
-3 -7 1 -4 8 3 7 -22 59 -55 116 -60 103 -68 122 -51 122 16 0 86 80 98 112
39 108 -9 228 -110 277 -37 18 -65 21 -235 23 l-192 3 0 -342z m385 313 c26
-5 74 -37 71 -49 -1 -6 -1 -9 1 -4 18 31 73 -77 73 -143 0 -56 -36 -127 -78
-155 -17 -11 -35 -30 -41 -42 -9 -19 -6 -27 14 -47 14 -14 25 -32 25 -41 0 -8
5 -15 11 -15 6 0 8 -9 4 -20 -4 -13 -2 -19 4 -18 12 3 68 -88 60 -96 -4 -4
-77 -7 -120 -5 -17 0 -54 64 -52 87 0 7 -1 11 -5 8 -6 -7 -32 41 -32 61 0 8
-4 12 -8 9 -4 -3 -17 11 -27 31 -21 38 -55 58 -67 39 -6 -8 -10 -8 -16 1 -6 9
-13 9 -32 0 -27 -14 -27 -12 -29 -150 l-1 -88 -57 2 -58 2 3 316 c2 235 6 317
15 323 10 6 301 1 342 -6z"/>
<path d="M3886 3328 c3 -4 -1 -8 -9 -8 -22 0 -37 -128 -21 -187 6 -22 11 -23
101 -23 91 0 96 1 124 29 41 41 43 113 5 146 -51 44 -67 55 -79 55 -8 0 -7 -4
3 -10 11 -7 -4 -10 -48 -10 -35 0 -61 4 -58 8 3 5 -1 9 -9 9 -8 0 -12 -4 -9
-9z m180 -49 c43 -43 31 -133 -19 -143 -12 -3 -57 -7 -99 -8 l-78 -3 0 93 0
94 85 -3 c75 -4 87 -7 111 -30z"/>
<path d="M4167 3099 c7 -7 15 -10 18 -7 3 3 -2 9 -12 12 -14 6 -15 5 -6 -5z"/>
<path d="M3986 3017 c3 -10 9 -15 12 -12 3 3 0 11 -7 18 -10 9 -11 8 -5 -6z"/>
<path d="M1643 2830 c0 -25 2 -35 4 -22 2 12 2 32 0 45 -2 12 -4 2 -4 -23z"/>
<path d="M2923 2373 c13 -4 17 -15 17 -59 0 -60 -16 -77 -51 -55 -11 6 -19 7
-19 2 0 -6 6 -13 13 -18 45 -28 81 10 75 82 -2 33 1 46 12 49 8 2 -3 4 -25 4
-22 0 -32 -2 -22 -5z"/>
<path d="M3072 2368 c-24 -24 -13 -49 28 -65 27 -11 40 -22 38 -32 -4 -20 -44
-27 -64 -10 -8 6 -17 9 -20 6 -3 -3 5 -12 18 -21 37 -24 87 -7 88 30 0 15 -19
29 -55 41 -34 11 -39 30 -13 47 13 8 23 8 38 -2 27 -16 35 -15 20 3 -15 18
-61 20 -78 3z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.8 KiB

16
packages/docs/ui/intro.md Normal file
View File

@@ -0,0 +1,16 @@
# SigPro UI
**SigPro UI** is a collection of high-performance **Web Components** built on top of the **SigPro** reactive library and styled with **DaisyUI**.
## Why SigPro UI?
Designed to streamline modern web development, SigPro UI combines the lightweight reactivity of SigPro with the beautiful, accessible design system of DaisyUI.
* **Native Web Components:** Use them in any framework or plain HTML.
* **Reactive by Design:** Powered by SigPro signals ($) for seamless state management.
* **Utility-First Styling:** Leveraging Tailwind CSS and DaisyUI for a polished look without the bloat.
* **Developer Experience:** Focus on building features, not reinventing UI patterns.
## Getting Started
SigPro UI allows you to build modular, reactive interfaces with minimal overhead, making web development faster, cleaner, and more efficient.

View File

@@ -0,0 +1,423 @@
# Vite Plugin: Automatic File-based Routing 🚦
SigPro provides an optional Vite plugin that automatically generates routes based on your file structure. No configuration needed - just create pages and they're instantly available with the correct paths.
## Why Use This Plugin?
While SigPro's router works perfectly with manually defined routes, this plugin:
- **Eliminates boilerplate** - No need to write route configurations
- **Enforces conventions** - Consistent URL structure across your app
- **Supports dynamic routes** - Use `[param]` syntax for parameters
- **Automatic code-splitting** - Each page becomes a separate chunk
- **Type-safe** (with JSDoc) - Routes follow your file structure
## Installation
The plugin is included with SigPro, but you need to add it to your Vite config:
```javascript
// vite.config.js
import { defineConfig } from 'vite';
import { sigproRouter } from 'sigpro';
export default defineConfig({
plugins: [sigproRouter()]
});
```
## How It Works
The plugin scans your `src/pages` directory and automatically generates routes based on the file structure:
```
src/pages/
├── index.js → '/'
├── about.js → '/about'
├── blog/
│ ├── index.js → '/blog'
│ └── [slug].js → '/blog/:slug'
└── users/
├── [id].js → '/users/:id'
└── [id]/edit.js → '/users/:id/edit'
```
## Usage
### 1. Enable the Plugin
Add the plugin to your Vite config as shown above.
### 2. Import the Generated Routes
Once you have the generated routes, using them with the router is straightforward:
```javascript
// main.js
import { $, html } from 'sigpro';
import { routes } from 'virtual:sigpro-routes';
// Simple usage
const router = $.router(routes);
document.body.appendChild(router);
```
Or directly in your template:
```javascript
// app.js
import { $, html } from 'sigpro';
import { routes } from 'virtual:sigpro-routes';
const App = () => html`
<div class="app">
<header>
<h1>My Application</h1>
</header>
<main class="p-4 flex flex-col gap-4 mx-auto w-full">
<div class="p-4 bg-base-100 rounded-box shadow-sm">
${$.router(routes)}
</div>
</main>
</div>
`;
document.body.appendChild(App());
```
This approach keeps your template clean and lets the router handle all the page rendering automatically.
### 3. Create Pages
```javascript
// src/pages/index.js
import { $, html } from 'sigpro';
export default () => {
return html`
<div>
<h1>Home Page</h1>
<a href="#/about">About</a>
</div>
`;
};
```
```javascript
// src/pages/users/[id].js
import { $, html } from 'sigpro';
export default (params) => {
const userId = params.id;
return html`
<div>
<h1>User Profile: ${userId}</h1>
<a href="#/users/${userId}/edit">Edit</a>
</div>
`;
};
```
## 📋 File-to-Route Mapping
### Static Routes
| File Path | Generated Route |
|-----------|-----------------|
| `src/pages/index.js` | `/` |
| `src/pages/about.js` | `/about` |
| `src/pages/contact/index.js` | `/contact` |
| `src/pages/blog/post.js` | `/blog/post` |
### Dynamic Routes
| File Path | Generated Route | Example URL |
|-----------|-----------------|-------------|
| `src/pages/users/[id].js` | `/users/:id` | `/users/42` |
| `src/pages/blog/[slug].js` | `/blog/:slug` | `/blog/hello-world` |
| `src/pages/users/[id]/posts/[pid].js` | `/users/:id/posts/:pid` | `/users/42/posts/123` |
### Nested Routes
| File Path | Generated Route | Notes |
|-----------|-----------------|-------|
| `src/pages/settings/index.js` | `/settings` | Index page |
| `src/pages/settings/profile.js` | `/settings/profile` | Sub-page |
| `src/pages/settings/security.js` | `/settings/security` | Sub-page |
| `src/pages/settings/[section].js` | `/settings/:section` | Dynamic section |
## 🎯 Advanced Examples
### Blog with Posts
```javascript
// src/pages/blog/index.js - Lists all posts
export default () => {
const posts = $([]);
$.effect(() => {
fetch('/api/posts')
.then(res => res.json())
.then(data => posts(data));
});
return html`
<div>
<h1>Blog</h1>
${posts().map(post => html`
<article>
<h2><a href="#/blog/${post.slug}">${post.title}</a></h2>
<p>${post.excerpt}</p>
</article>
`)}
</div>
`;
};
```
```javascript
// src/pages/blog/[slug].js - Single post
export default (params) => {
const post = $(null);
const slug = params.slug;
$.effect(() => {
fetch(`/api/posts/${slug}`)
.then(res => res.json())
.then(data => post(data));
});
return html`
<div>
<a href="#/blog">← Back to blog</a>
${() => post() ? html`
<article>
<h1>${post().title}</h1>
<div>${post().content}</div>
</article>
` : html`<div>Loading...</div>`}
</div>
`;
};
```
### Dashboard with Nested Sections
```javascript
// src/pages/dashboard/index.js
export default () => {
return html`
<div class="dashboard">
<nav>
<a href="#/dashboard">Overview</a>
<a href="#/dashboard/analytics">Analytics</a>
<a href="#/dashboard/settings">Settings</a>
</nav>
<main>
<h1>Dashboard Overview</h1>
<!-- Overview content -->
</main>
</div>
`;
};
```
```javascript
// src/pages/dashboard/analytics.js
export default () => {
return html`
<div class="dashboard">
<nav>
<a href="#/dashboard">Overview</a>
<a href="#/dashboard/analytics">Analytics</a>
<a href="#/dashboard/settings">Settings</a>
</nav>
<main>
<h1>Analytics</h1>
<!-- Analytics content -->
</main>
</div>
`;
};
```
### E-commerce Product Routes
```javascript
// src/pages/products/[category]/[id].js
export default (params) => {
const { category, id } = params;
const product = $(null);
$.effect(() => {
fetch(`/api/products/${category}/${id}`)
.then(res => res.json())
.then(data => product(data));
});
return html`
<div class="product-page">
<nav class="breadcrumbs">
<a href="#/products">Products</a> &gt;
<a href="#/products/${category}">${category}</a> &gt;
<span>${id}</span>
</nav>
${() => product() ? html`
<div class="product">
<h1>${product().name}</h1>
<p class="price">$${product().price}</p>
<p>${product().description}</p>
<button @click=${() => addToCart(product())}>
Add to Cart
</button>
</div>
` : html`<div>Loading...</div>`}
</div>
`;
};
```
## 🔧 Configuration Options
The plugin accepts an optional configuration object:
```javascript
// vite.config.js
import { defineConfig } from 'vite';
import { sigproRouter } from 'sigpro/vite';
export default defineConfig({
plugins: [
sigproRouter({
pagesDir: 'src/pages', // Default: 'src/pages'
extensions: ['.js', '.jsx'], // Default: ['.js', '.jsx']
exclude: ['**/_*', '**/components/**'] // Glob patterns to exclude
})
]
});
```
### Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `pagesDir` | `string` | `'src/pages'` | Directory containing your pages |
| `extensions` | `string[]` | `['.js', '.jsx']` | File extensions to include |
| `exclude` | `string[]` | `[]` | Glob patterns to exclude |
## 🎯 Route Priority
The plugin automatically sorts routes to ensure correct matching:
1. **Static routes** take precedence over dynamic ones
2. **More specific routes** (deeper paths) come first
3. **Alphabetical order** for routes at the same level
Example sorting:
```
/users/new (static, specific)
/users/[id]/edit (dynamic, deeper)
/users/[id] (dynamic, shallower)
/users/profile (static, shallower)
```
## 📦 Output Example
When you import `virtual:sigpro-routes`, you get:
```javascript
// Generated module
import Page_0 from '/src/pages/index.js';
import Page_1 from '/src/pages/about.js';
import Page_2 from '/src/pages/blog/index.js';
import Page_3 from '/src/pages/blog/[slug].js';
import Page_4 from '/src/pages/users/[id].js';
import Page_5 from '/src/pages/users/[id]/edit.js';
export const routes = [
{ path: '/', component: Page_0 },
{ path: '/about', component: Page_1 },
{ path: '/blog', component: Page_2 },
{ path: '/blog/:slug', component: Page_3 },
{ path: '/users/:id', component: Page_4 },
{ path: '/users/:id/edit', component: Page_5 },
];
```
## 🚀 Performance Benefits
- **Automatic code splitting** - Each page becomes a separate chunk
- **Lazy loading ready** - Import pages dynamically
- **Tree shaking** - Only used routes are included
```javascript
// With dynamic imports (automatic with Vite)
const routes = [
{ path: '/', component: () => import('./pages/index.js') },
{ path: '/about', component: () => import('./pages/about.js') },
// ...
];
```
## 💡 Pro Tips
### 1. Group Related Pages
```
src/pages/
├── dashboard/
│ ├── index.js
│ ├── analytics.js
│ └── settings.js
└── dashboard.js # ❌ Don't mix with folder
```
### 2. Use Index Files for Clean URLs
```
✅ Good:
pages/blog/index.js → /blog
pages/blog/post.js → /blog/post
❌ Avoid:
pages/blog.js → /blog (conflicts with folder)
```
### 3. Private Components
Prefix with underscore to exclude from routing:
```
src/pages/
├── index.js
├── about.js
└── _components/ # ❌ Not scanned
└── Header.js
```
### 4. Layout Components
Create a layout wrapper in your main entry:
```javascript
// main.js
import { $, html } from 'sigpro';
import { routes } from 'virtual:sigpro-routes';
// Wrap all routes with layout
const routesWithLayout = routes.map(route => ({
...route,
component: (params) => Layout(route.component(params))
}));
const router = $.router(routesWithLayout);
document.body.appendChild(router);
```
---
> **Note:** This plugin is completely optional. You can always define routes manually if you prefer. The plugin just saves you from writing boilerplate route configurations.
> **Pro Tip:** The plugin works great with hot module replacement (HMR) - add a new page and it's instantly available in your dev server without restarting!

91
packages/sigpro/plugin.js Normal file
View File

@@ -0,0 +1,91 @@
// plugins/sigpro-plugin-router.js
import fs from 'fs';
import path from 'path';
export default function sigproRouter() {
const virtualModuleId = 'virtual:sigpro-routes';
const resolvedVirtualModuleId = '\0' + virtualModuleId;
function getFiles(dir) {
let results = [];
if (!fs.existsSync(dir)) return results;
const list = fs.readdirSync(dir);
list.forEach(file => {
const fullPath = path.resolve(dir, file);
const stat = fs.statSync(fullPath);
if (stat && stat.isDirectory()) {
results = results.concat(getFiles(fullPath));
} else if (file.endsWith('.js') || file.endsWith('.jsx')) {
results.push(fullPath);
}
});
return results;
}
function filePathToUrl(relativePath) {
let url = relativePath.replace(/\\/g, '/').replace(/\.jsx?$/, '');
if (url === 'index') {
return '/';
}
if (url.endsWith('/index')) {
url = url.slice(0, -6);
}
url = url.replace(/\[([^\]]+)\]/g, ':$1');
let finalPath = '/' + url.toLowerCase();
return finalPath.replace(/\/+/g, '/').replace(/\/$/, '') || '/';
}
return {
name: 'sigpro-router',
resolveId(id) {
if (id === virtualModuleId) return resolvedVirtualModuleId;
},
load(id) {
if (id === resolvedVirtualModuleId) {
const pagesDir = path.resolve(process.cwd(), 'src/pages');
let files = getFiles(pagesDir);
files = files.sort((a, b) => {
const aRel = path.relative(pagesDir, a).replace(/\\/g, '/');
const bRel = path.relative(pagesDir, b).replace(/\\/g, '/');
const aDynamic = aRel.includes('[') || aRel.includes(':');
const bDynamic = bRel.includes('[') || bRel.includes(':');
if (aDynamic !== bDynamic) return aDynamic ? 1 : -1;
return bRel.length - aRel.length;
});
let imports = '';
let routeArray = 'export const routes = [\n';
console.log('\n🚀 [SigPro Router] Routes generated:');
files.forEach((fullPath, i) => {
const importPath = fullPath.replace(/\\/g, '/');
const relativePath = path.relative(pagesDir, fullPath).replace(/\\/g, '/');
const varName = `Page_${i}`;
let urlPath = filePathToUrl(relativePath);
const isDynamic = urlPath.includes(':');
imports += `import ${varName} from '${importPath}';\n`;
console.log(` ${isDynamic ? '🔗' : '📄'} ${urlPath.padEnd(30)} -> ${relativePath}`);
routeArray += ` { path: '${urlPath}', component: ${varName} },\n`;
});
routeArray += '];';
return `${imports}\n${routeArray}`;
}
}
};
}

1
packages/sigpro/plugin.min.js vendored Normal file
View File

@@ -0,0 +1 @@
import fs from"fs";import path from"path";export default function sigproRouter(){const e="virtual:sigpro-routes",r="\0"+e;function t(e){let r=[];if(!fs.existsSync(e))return r;return fs.readdirSync(e).forEach((n=>{const s=path.resolve(e,n),o=fs.statSync(s);o&&o.isDirectory()?r=r.concat(t(s)):(n.endsWith(".js")||n.endsWith(".jsx"))&&r.push(s)})),r}return{name:"sigpro-router",resolveId(t){if(t===e)return r},load(e){if(e===r){const e=path.resolve(process.cwd(),"src/pages");let r=t(e);r=r.sort(((r,t)=>{const n=path.relative(e,r).replace(/\\/g,"/"),s=path.relative(e,t).replace(/\\/g,"/"),o=n.includes("[")||n.includes(":");return o!==(s.includes("[")||s.includes(":"))?o?1:-1:s.length-n.length}));let n="",s="export const routes = [\n";return r.forEach(((r,t)=>{const o=r.replace(/\\/g,"/"),c=`Page_${t}`;let a=function(e){let r=e.replace(/\\/g,"/").replace(/\.jsx?$/,"");return"index"===r?"/":(r.endsWith("/index")&&(r=r.slice(0,-6)),r=r.replace(/\[([^\]]+)\]/g,":$1"),("/"+r.toLowerCase()).replace(/\/+/g,"/").replace(/\/$/,"")||"/")}(path.relative(e,r).replace(/\\/g,"/"));a.includes(":");n+=`import ${c} from '${o}';\n`,s+=` { path: '${a}', component: ${c} },\n`})),s+="];",`${n}\n${s}`}}}}

631
packages/sigpro/sigpro.js Normal file
View File

@@ -0,0 +1,631 @@
// Global state for tracking the current reactive effect
let activeEffect = null;
const effectQueue = new Set();
let isFlushScheduled = false;
let flushCount = 0;
const flushEffectQueue = () => {
isFlushScheduled = false;
flushCount++;
if (flushCount > 100) {
effectQueue.clear();
flushCount = 0;
throw new Error("SigPro: Infinite reactive loop detected.");
}
try {
const effects = Array.from(effectQueue);
effectQueue.clear();
for (const effect of effects) effect.run();
} catch (error) {
console.error("SigPro Flush Error:", error);
} finally {
setTimeout(() => {
flushCount = 0;
}, 0);
}
};
/**
* Creates a reactive signal
* @param {any} initialValue - Initial value or getter function
* @returns {Function} Signal getter/setter function
*/
const $ = (initialValue) => {
const subscribers = new Set();
let signal;
if (typeof initialValue === "function") {
let isDirty = true;
let cachedValue;
const computedEffect = {
dependencies: new Set(),
markDirty: () => {
if (!isDirty) {
isDirty = true;
subscribers.forEach((sub) => {
if (sub.markDirty) sub.markDirty();
effectQueue.add(sub);
});
if (!isFlushScheduled && effectQueue.size) {
isFlushScheduled = true;
queueMicrotask(flushEffectQueue);
}
}
},
run: () => {
computedEffect.dependencies.forEach((dep) => dep.delete(computedEffect));
computedEffect.dependencies.clear();
const prev = activeEffect;
activeEffect = computedEffect;
try {
cachedValue = initialValue();
} finally {
activeEffect = prev;
isDirty = false;
}
},
};
signal = () => {
if (activeEffect) {
subscribers.add(activeEffect);
activeEffect.dependencies.add(subscribers);
}
if (isDirty) computedEffect.run();
return cachedValue;
};
} else {
signal = (...args) => {
if (args.length) {
const next = typeof args[0] === "function" ? args[0](initialValue) : args[0];
if (!Object.is(initialValue, next)) {
initialValue = next;
subscribers.forEach((sub) => {
if (sub.markDirty) sub.markDirty();
effectQueue.add(sub);
});
if (!isFlushScheduled && effectQueue.size) {
isFlushScheduled = true;
queueMicrotask(flushEffectQueue);
}
}
}
if (activeEffect) {
subscribers.add(activeEffect);
activeEffect.dependencies.add(subscribers);
}
return initialValue;
};
}
return signal;
};
let currentPageCleanups = null;
/**
* Creates a reactive effect that runs when dependencies change
* @param {Function} effectFn - The effect function to run
* @returns {Function} Cleanup function to stop the effect
*/
const $e = (effectFn) => {
const effect = {
dependencies: new Set(),
cleanupHandlers: new Set(),
run() {
this.cleanupHandlers.forEach((h) => h());
this.cleanupHandlers.clear();
this.dependencies.forEach((dep) => dep.delete(this));
this.dependencies.clear();
const prev = activeEffect;
activeEffect = this;
try {
const res = effectFn();
if (typeof res === "function") this.cleanupHandlers.add(res);
} finally {
activeEffect = prev;
}
},
stop() {
this.cleanupHandlers.forEach((h) => h());
this.dependencies.forEach((dep) => dep.delete(this));
},
};
if (currentPageCleanups) currentPageCleanups.push(() => effect.stop());
if (activeEffect) activeEffect.cleanupHandlers.add(() => effect.stop());
effect.run();
return () => effect.stop();
};
/**
* Persistent signal with localStorage
* @param {string} key - Storage key
* @param {any} initialValue - Default value if none stored
* @param {Storage} [storage=localStorage] - Storage type (localStorage/sessionStorage)
* @returns {Function} Signal that persists to storage
*/
const $s = (key, initialValue, storage = localStorage) => {
let initial;
try {
const saved = storage.getItem(key);
if (saved !== null) {
initial = JSON.parse(saved);
} else {
initial = initialValue;
}
} catch (e) {
console.warn(`Error reading ${key} from storage:`, e);
initial = initialValue;
storage.removeItem(key);
}
const signal = $(initial);
$e(() => {
try {
const value = signal();
if (value === undefined || value === null) {
storage.removeItem(key);
} else {
storage.setItem(key, JSON.stringify(value));
}
} catch (e) {
console.warn(`Error saving ${key} to storage:`, e);
}
});
return signal;
};
/**
* Tagged template literal for creating reactive HTML
* @param {string[]} strings - Template strings
* @param {...any} values - Dynamic values
* @returns {DocumentFragment} Reactive document fragment
* @see {@link https://developer.mozilla.org/es/docs/Glossary/Cross-site_scripting}
*/
const html = (strings, ...values) => {
const templateCache = html._templateCache ?? (html._templateCache = new WeakMap());
const getNodeByPath = (root, path) => path.reduce((node, index) => node?.childNodes?.[index], root);
const applyTextContent = (node, values) => {
const parts = node.textContent.split("{{part}}");
const parent = node.parentNode;
let valueIndex = 0;
parts.forEach((part, index) => {
if (part) parent.insertBefore(document.createTextNode(part), node);
if (index < parts.length - 1) {
const currentValue = values[valueIndex++];
const startMarker = document.createComment("s");
const endMarker = document.createComment("e");
parent.insertBefore(startMarker, node);
parent.insertBefore(endMarker, node);
if (typeof currentValue === "function") {
let lastResult;
$e(() => {
const result = currentValue();
if (result === lastResult) return;
lastResult = result;
updateContent(result);
});
} else {
updateContent(currentValue);
}
function updateContent(result) {
if (typeof result !== "object" && !Array.isArray(result)) {
const textNode = startMarker.nextSibling;
const safeText = String(result ?? "");
if (textNode !== endMarker && textNode?.nodeType === 3) {
textNode.textContent = safeText;
} else {
while (startMarker.nextSibling !== endMarker) parent.removeChild(startMarker.nextSibling);
parent.insertBefore(document.createTextNode(safeText), endMarker);
}
} else {
while (startMarker.nextSibling !== endMarker) parent.removeChild(startMarker.nextSibling);
const items = Array.isArray(result) ? result : [result];
const fragment = document.createDocumentFragment();
items.forEach((item) => {
if (item == null || item === false) return;
const nodeItem = item instanceof Node ? item : document.createTextNode(item);
fragment.appendChild(nodeItem);
});
parent.insertBefore(fragment, endMarker);
}
}
}
});
node.remove();
};
let cachedTemplate = templateCache.get(strings);
if (!cachedTemplate) {
const template = document.createElement("template");
template.innerHTML = strings.join("{{part}}");
const dynamicNodes = [];
const treeWalker = document.createTreeWalker(template.content, 133);
const getNodePath = (node) => {
const path = [];
while (node && node !== template.content) {
let index = 0;
for (let sibling = node.previousSibling; sibling; sibling = sibling.previousSibling) index++;
path.push(index);
node = node.parentNode;
}
return path.reverse();
};
let currentNode;
while ((currentNode = treeWalker.nextNode())) {
let isDynamic = false;
const nodeInfo = {
type: currentNode.nodeType,
path: getNodePath(currentNode),
parts: [],
};
if (currentNode.nodeType === 1) {
for (let i = 0; i < currentNode.attributes.length; i++) {
const attribute = currentNode.attributes[i];
if (attribute.value.includes("{{part}}")) {
nodeInfo.parts.push({ name: attribute.name });
isDynamic = true;
}
}
} else if (currentNode.nodeType === 3 && currentNode.textContent.includes("{{part}}")) {
isDynamic = true;
}
if (isDynamic) dynamicNodes.push(nodeInfo);
}
templateCache.set(strings, (cachedTemplate = { template, dynamicNodes }));
}
const fragment = cachedTemplate.template.content.cloneNode(true);
let valueIndex = 0;
const targets = cachedTemplate.dynamicNodes.map((nodeInfo) => ({
node: getNodeByPath(fragment, nodeInfo.path),
info: nodeInfo,
}));
targets.forEach(({ node, info }) => {
if (!node) return;
if (info.type === 1) {
info.parts.forEach((part) => {
const currentValue = values[valueIndex++];
const attributeName = part.name;
const firstChar = attributeName[0];
if (firstChar === "@") {
const [eventName, ...modifiers] = attributeName.slice(1).split(".");
const handlerWrapper = (e) => {
if (modifiers.includes("prevent")) e.preventDefault();
if (modifiers.includes("stop")) e.stopPropagation();
if (modifiers.includes("self") && e.target !== node) return;
if (modifiers.some((m) => m.startsWith("debounce"))) {
const ms = modifiers.find((m) => m.startsWith("debounce"))?.split(":")[1] || 300;
clearTimeout(node._debounceTimer);
node._debounceTimer = setTimeout(() => currentValue(e), ms);
return;
}
if (modifiers.includes("once")) {
node.removeEventListener(eventName, handlerWrapper);
}
currentValue(e);
};
node.addEventListener(eventName, handlerWrapper, {
passive: modifiers.includes("passive"),
capture: modifiers.includes("capture"),
});
} else if (firstChar === ":") {
const propertyName = attributeName.slice(1);
const eventType = node.type === "checkbox" || node.type === "radio" ? "change" : "input";
if (typeof currentValue === "function") {
$e(() => {
const value = currentValue();
if (node[propertyName] !== value) node[propertyName] = value;
});
} else {
node[propertyName] = currentValue;
}
node.addEventListener(eventType, () => {
const value = eventType === "change" ? node.checked : node.value;
if (typeof currentValue === "function") currentValue(value);
});
} else if (firstChar === "?") {
const attrName = attributeName.slice(1);
if (typeof currentValue === "function") {
$e(() => {
const result = currentValue();
node.toggleAttribute(attrName, !!result);
});
} else {
node.toggleAttribute(attrName, !!currentValue);
}
} else if (firstChar === ".") {
const propertyName = attributeName.slice(1);
if (typeof currentValue === "function") {
$e(() => {
const result = currentValue();
node[propertyName] = result;
if (result != null && typeof result !== "object" && typeof result !== "boolean") {
node.setAttribute(propertyName, result);
}
});
} else {
node[propertyName] = currentValue;
if (currentValue != null && typeof currentValue !== "object" && typeof currentValue !== "boolean") {
node.setAttribute(propertyName, currentValue);
}
}
} else {
if (typeof currentValue === "function") {
$e(() => node.setAttribute(attributeName, currentValue()));
} else {
node.setAttribute(attributeName, currentValue);
}
}
});
} else if (info.type === 3) {
const placeholderCount = node.textContent.split("{{part}}").length - 1;
applyTextContent(node, values.slice(valueIndex, valueIndex + placeholderCount));
valueIndex += placeholderCount;
}
});
return fragment;
};
/**
* Creates a page with automatic cleanup
* @param {Function} setupFunction - Page setup function that receives props
* @returns {Function} A function that creates page instances with props
*/
const $p = (setupFunction) => {
const tagName = "page-" + Math.random().toString(36).substring(2, 9);
customElements.define(
tagName,
class extends HTMLElement {
connectedCallback() {
this.style.display = "contents";
this._cleanups = [];
currentPageCleanups = this._cleanups;
try {
const result = setupFunction({
params: JSON.parse(this.getAttribute("params") || "{}"),
onUnmount: (fn) => this._cleanups.push(fn),
});
this.appendChild(result instanceof Node ? result : document.createTextNode(String(result)));
} finally {
currentPageCleanups = null;
}
}
disconnectedCallback() {
this._cleanups.forEach((fn) => fn());
this._cleanups = [];
this.innerHTML = "";
}
},
);
return (props = {}) => {
const el = document.createElement(tagName);
el.setAttribute("params", JSON.stringify(props));
return el;
};
};
/**
* Creates a custom web component with reactive properties
* @param {string} tagName - Custom element tag name
* @param {Function} setupFunction - Component setup function
* @param {string[]} observedAttributes - Array of observed attributes
* @param {boolean} useShadowDOM - Enable Shadow DOM (default: false)
*/
const $c = (tagName, setupFunction, observedAttributes = [], useShadowDOM = false) => {
if (customElements.get(tagName)) return;
customElements.define(
tagName,
class extends HTMLElement {
static get observedAttributes() {
return observedAttributes;
}
constructor() {
super();
this._propertySignals = {};
this.cleanupFunctions = [];
if (useShadowDOM) {
this._root = this.attachShadow({ mode: "open" });
} else {
this._root = this;
}
observedAttributes.forEach((attr) => (this._propertySignals[attr] = $(undefined)));
}
connectedCallback() {
const frozenChildren = [...this.childNodes];
this._root.innerHTML = "";
observedAttributes.forEach((attr) => {
const initialValue = this.hasOwnProperty(attr) ? this[attr] : this.getAttribute(attr);
Object.defineProperty(this, attr, {
get: () => this._propertySignals[attr](),
set: (value) => {
const processedValue = value === "false" ? false : value === "" && attr !== "value" ? true : value;
this._propertySignals[attr](processedValue);
},
configurable: true,
});
if (initialValue !== null && initialValue !== undefined) this[attr] = initialValue;
});
const context = {
select: (selector) => this._root.querySelector(selector),
selectAll: (selector) => this._root.querySelectorAll(selector),
slot: (name) =>
frozenChildren.filter((node) => {
const slotName = node.nodeType === 1 ? node.getAttribute("slot") : null;
return name ? slotName === name : !slotName;
}),
emit: (name, detail) => this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true })),
host: this,
root: this._root,
onUnmount: (cleanupFn) => this.cleanupFunctions.push(cleanupFn),
};
const result = setupFunction(this._propertySignals, context);
if (result instanceof Node) this._root.appendChild(result);
}
attributeChangedCallback(name, oldValue, newValue) {
if (this[name] !== newValue) this[name] = newValue;
}
disconnectedCallback() {
this.cleanupFunctions.forEach((cleanupFn) => cleanupFn());
this.cleanupFunctions = [];
}
},
);
};
/**
* Ultra-simple fetch wrapper with optional loading signal
* @param {string} url - Endpoint URL
* @param {Object} data - Data to send (automatically JSON.stringify'd)
* @param {Function} [loading] - Optional signal function to track loading state
* @returns {Promise<Object|null>} Parsed JSON response or null on error
*/
const $f = async (url, data, loading) => {
if (loading) loading(true);
try {
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
const text = await res.text();
try {
return JSON.parse(text);
} catch (e) {
console.warn("Invalid JSON response");
return null;
}
} catch (e) {
return null;
} finally {
if (loading) loading(false);
}
};
/**
* Creates a router for hash-based navigation
* @param {Array<{path: string|RegExp, component: Function}>} routes - Route configurations
* @returns {HTMLDivElement} Router container element
*/
const $r = (routes) => {
const getCurrentPath = () => window.location.hash.replace(/^#/, "") || "/";
const container = document.createElement("div");
container.style.display = "contents";
const matchRoute = (path, routePath) => {
if (!routePath.includes(":")) {
return routePath === path ? {} : null;
}
const parts = routePath.split("/");
const pathParts = path.split("/");
if (parts.length !== pathParts.length) return null;
const params = {};
for (let i = 0; i < parts.length; i++) {
if (parts[i].startsWith(":")) {
params[parts[i].slice(1)] = pathParts[i];
} else if (parts[i] !== pathParts[i]) {
return null;
}
}
return params;
};
const render = () => {
const path = getCurrentPath();
let matchedRoute = null;
let routeParams = {};
for (const route of routes) {
const params = matchRoute(path, route.path);
if (params !== null) {
matchedRoute = route;
routeParams = params;
break;
}
}
const view = matchedRoute ? matchedRoute.component(routeParams) : Object.assign(document.createElement("h1"), { textContent: "404" });
container.replaceChildren(view instanceof Node ? view : document.createTextNode(String(view ?? "")));
};
window.addEventListener("hashchange", render);
render();
return container;
};
$r.go = (path) => {
const targetPath = path.startsWith("/") ? path : `/${path}`;
if (window.location.hash !== `#${targetPath}`) {
window.location.hash = targetPath;
}
};
/* Can customize the name of your functions */
$.effect = $e;
$.page = $p;
$.component = $c;
$.fetch = $f;
$.router = $r;
$.storage = $s;
if (typeof window !== "undefined") {
window.$ = $;
}
export { $, html };

1
packages/sigpro/sigpro.min.js vendored Normal file

File diff suppressed because one or more lines are too long

244
packages/sigproui/app.js Normal file
View File

@@ -0,0 +1,244 @@
/**
* UI Demo/Test - Para probar componentes localmente sin hacer release
*
* Ejecutar:
* 1. Crear un archivo index.html que importe este archivo
* 2. O usar con Vite: bun add -d vite && bun vite UI/app.js
*
* Alternativamente, simplemente copiar las partes que necesitas a tu proyecto
*/
import { $, html, effect } from "../../index.js";
import { Button, Input, Card, Drawer, Menu, Dropdown, Fab, Dialog, Loading } from "./index.js";
// Importar la función helper de loading
import { loading } from "./components/Loading.js";
// Estado para la demo
const state = {
inputValue: $(""),
checkboxValue: $(false),
radioValue: $("option1"),
rangeValue: $(50),
showDialog: $(false),
openDrawer: $(false),
};
// Menú de navegación
const menuItems = [
{ label: "Home", icon: "icon-[lucide--home]", href: "#/home" },
{ label: "About", icon: "icon-[lucide--info]", href: "#/about" },
{
label: "Components",
icon: "icon-[lucide--box]",
open: false,
sub: [
{ label: "Button", href: "#/button" },
{ label: "Input", href: "#/input" },
{ label: "Card", href: "#/card" },
{ label: "Forms", href: "#/forms" },
]
},
];
// Demo page principal
export default function App() {
effect(() => {
console.log("Input value:", state.inputValue());
});
return html`
<div class="min-h-screen bg-base-100 text-base-content p-4">
<header class="navbar bg-base-200 shadow-xl px-4 mb-6 rounded-lg">
<div class="flex-1">
<span class="text-xl font-bold">SigProUI Test</span>
</div>
<div class="flex-none">
<c-button @click=${() => state.openDrawer(!state.openDrawer())}>
<span class="icon-[lucide--menu]"></span>
</c-button>
</div>
</header>
<c-drawer .open=${state.openDrawer}>
<c-menu .items=${menuItems}></c-menu>
</c-drawer>
<main class="max-w-4xl mx-auto space-y-8">
<!-- Buttons Section -->
<section class="space-y-4">
<h2 class="text-2xl font-bold">Buttons</h2>
<div class="flex flex-wrap gap-2">
<c-button @click=${() => console.log("Primary clicked")}>Primary</c-button>
<c-button ui="btn-secondary">Secondary</c-button>
<c-button ui="btn-accent">Accent</c-button>
<c-button ui="btn-ghost">Ghost</c-button>
<c-button ui="btn-link">Link</c-button>
<c-button .loading=${true}>Loading</c-button>
<c-button .disabled=${true}>Disabled</c-button>
<c-button .badge=${"5"}>With Badge</c-button>
<c-button .tooltip=${"I'm a tooltip!"}>With Tooltip</c-button>
</div>
</section>
<!-- Input Section -->
<section class="space-y-4">
<h2 class="text-2xl font-bold">Input</h2>
<div class="flex flex-col gap-4 max-w-md">
<c-input
label="Username"
.value=${state.inputValue}
@input=${(v) => state.inputValue(v)}
placeholder="Enter username"
></c-input>
<c-input
label="Email"
type="email"
placeholder="email@example.com"
icon="icon-[lucide--mail]"
></c-input>
<c-input
label="Password"
type="password"
placeholder="••••••••"
icon="icon-[lucide--lock]"
></c-input>
</div>
<p class="text-sm opacity-70">Current input value: "${state.inputValue()}"</p>
</section>
<!-- Card Section -->
<section class="space-y-4">
<h2 class="text-2xl font-bold">Card</h2>
<c-card bordered>
<span slot="title">Card Title</span>
<p>This is a basic card with some content.</p>
<div slot="actions">
<c-button ui="btn-sm btn-primary">Accept</c-button>
<c-button ui="btn-sm">Cancel</c-button>
</div>
</c-card>
<c-card .img=${"https://img.daisyui.com/images/stock/photo-1606107557195-0e29a4b5b4aa.webp"} bordered>
<span slot="title">Card with Image</span>
<p>Beautiful flower image</p>
<div slot="actions">
<c-button ui="btn-primary">Buy Now</c-button>
</div>
</c-card>
</section>
<!-- Forms Section -->
<section class="space-y-4">
<h2 class="text-2xl font-bold">Form Controls</h2>
<div class="flex flex-wrap gap-6">
<c-check
.checked=${state.checkboxValue}
label="Accept terms"
@change=${(v) => state.checkboxValue(v)}
></c-check>
<p class="text-sm">Checkbox value: ${() => state.checkboxValue() ? "checked" : "unchecked"}</p>
<div class="flex flex-col gap-2">
<c-radio
name="radio-demo"
.checked=${() => state.radioValue() === "option1"}
.value=${"option1"}
label="Option 1"
@change=${(v) => state.radioValue(v)}
></c-radio>
<c-radio
name="radio-demo"
.checked=${() => state.radioValue() === "option2"}
.value=${"option2"}
label="Option 2"
@change=${(v) => state.radioValue(v)}
></c-radio>
<c-radio
name="radio-demo"
.checked=${() => state.radioValue() === "option3"}
.value=${"option3"}
label="Option 3"
@change=${(v) => state.radioValue(v)}
></c-radio>
</div>
<p class="text-sm">Radio value: "${state.radioValue()}"</p>
<div class="w-full max-w-xs">
<c-range
.value=${state.rangeValue}
min="0" max="100"
@change=${(v) => state.rangeValue(Number(v))}
></c-range>
<p class="text-sm">Range value: ${state.rangeValue()}</p>
</div>
</div>
</section>
<!-- Loading Section -->
<section class="space-y-4">
<h2 class="text-2xl font-bold">Loading</h2>
<div class="flex gap-4">
<c-button @click=${() => loading(true, "Loading...")}>
Show Loading
</c-button>
<c-button @click=${() => loading(false)}>
Hide Loading
</c-button>
</div>
</section>
<!-- Dialog Section -->
<section class="space-y-4">
<h2 class="text-2xl font-bold">Dialog</h2>
<c-button @click=${() => state.showDialog(true)}>Open Dialog</c-button>
<c-dialog .open=${state.showDialog} @close=${() => state.showDialog(false)}>
<span slot="title" class="font-bold text-lg">Confirm Action</span>
<p>Are you sure you want to proceed?</p>
<div slot="buttons" class="flex gap-2 justify-end">
<c-button ui="btn-ghost" @click=${() => state.showDialog(false)}>Cancel</c-button>
<c-button ui="btn-primary" @click=${() => { console.log("Confirmed!"); state.showDialog(false); }}>Confirm</c-button>
</div>
</c-dialog>
</section>
<!-- Dropdown Section -->
<section class="space-y-4">
<h2 class="text-2xl font-bold">Dropdown</h2>
<c-dropdown>
<c-button slot="trigger">Open Dropdown</c-button>
<li><a>Item 1</a></li>
<li><a>Item 2</a></li>
<li><a>Item 3</a></li>
</c-dropdown>
</section>
<!-- FAB Section -->
<section class="space-y-4">
<h2 class="text-2xl font-bold">FAB (Floating Action Button)</h2>
<c-fab
.actions=${[
{ label: "Add", icon: "icon-[lucide--plus]", ui: "btn-secondary" },
{ label: "Edit", icon: "icon-[lucide--edit]", ui: "btn-accent" },
{ label: "Message", icon: "icon-[lucide--message]", ui: "btn-info" },
]}
></c-fab>
</section>
</main>
</div>
`;
}
// Mount the app
if (typeof document !== "undefined") {
const root = document.getElementById("app");
if (root) {
root.appendChild(App());
}
}

View File

@@ -0,0 +1,31 @@
import { $, html } from "sigpro";
$.component(
"c-button",
(props, { emit, slot }) => {
const spinner = () => html`
<span .class="${() => `loading loading-spinner loading-xs ${props.loading() ? "" : "hidden"}`}"></span>
`;
return html`
<div class="${props.tooltip() ? "tooltip" : ""}" data-tip=${() => props.tooltip() ?? ""}>
<button
class="${() => `btn ${props.ui() ?? ""} ${props.badge() ? "indicator" : ""}`}"
?disabled=${() => props.disabled()}
@click=${(e) => {
e.stopPropagation();
if (!props.loading() && !props.disabled()) emit("click", e);
}}>
${spinner()} ${slot()}
${() =>
props.badge()
? html`
<span class="indicator-item badge badge-secondary">${props.badge()}</span>
`
: null}
</button>
</div>
`;
},
["ui", "loading", "badge", "tooltip", "disabled"],
);

View File

@@ -0,0 +1,26 @@
import { $, html } from "sigpro";
$.component(
"c-card",
(props, host) => {
return html`
<div class="${() => `card bg-base-100 shadow-sm ${props.ui() ?? ""}`}">
${() =>
props.img()
? html`
<figure>
<img src="${() => props.img()}" alt="${() => props.alt() ?? "Card image"}" />
</figure>
`
: null}
<div class="card-body">
<h2 class="card-title">${host.slot("title")}</h2>
<div class="card-content">${host.slot("body")}</div>
<div class="card-actions justify-end">${host.slot("actions")}</div>
</div>
</div>
`;
},
["img", "alt", "ui"],
);

View File

@@ -0,0 +1,50 @@
import { $, html } from "sigpro";
const getVal = (props, key, def) => {
const v = props[key];
if (v === undefined || v === null) return def;
if (typeof v === "function") {
try {
return v();
} catch {
return def;
}
}
return v;
};
const toString = (val) => {
if (val === undefined || val === null) return "";
return String(val);
};
$.component(
"c-check",
(props, { emit }) => {
const label = toString(getVal(props, "label", ""));
const disabled = getVal(props, "disabled", false);
const isToggle = getVal(props, "toggle", false);
return html`
<label class="label cursor-pointer flex gap-2">
<input
type="checkbox"
class="${isToggle ? "toggle" : "checkbox"}"
?disabled="${disabled}"
.checked=${() => getVal(props, "checked", false)}
@change="${(e) => {
if (disabled) return;
const val = e.target.checked;
if (typeof props.checked === "function") props.checked(val);
emit("change", val);
}}" />
${label
? html`
<span class="label-text">${label}</span>
`
: ""}
</label>
`;
},
["label", "checked", "disabled", "toggle"],
);

View File

@@ -0,0 +1,65 @@
import { $, html } from "sigpro";
const p1 = ["#000", "#1A1A1A", "#333", "#4D4D4D", "#666", "#808080", "#B3B3B3", "#FFF"];
const p2 = ["#450a0a", "#7f1d1d", "#991b1b", "#b91c1c", "#dc2626", "#ef4444", "#f87171", "#fca5a5"];
const p3 = ["#431407", "#7c2d12", "#9a3412", "#c2410c", "#ea580c", "#f97316", "#fb923c", "#ffedd5"];
const p4 = ["#713f12", "#a16207", "#ca8a04", "#eab308", "#facc15", "#fde047", "#fef08a", "#fff9c4"];
const p5 = ["#064e3b", "#065f46", "#059669", "#10b981", "#34d399", "#4ade80", "#84cc16", "#d9f99d"];
const p6 = ["#082f49", "#075985", "#0284c7", "#0ea5e9", "#38bdf8", "#7dd3fc", "#22d3ee", "#cffafe"];
const p7 = ["#1e1b4b", "#312e81", "#4338ca", "#4f46e5", "#6366f1", "#818cf8", "#a5b4fc", "#e0e7ff"];
const p8 = ["#2e1065", "#4c1d95", "#6d28d9", "#7c3aed", "#8b5cf6", "#a855f7", "#d946ef", "#fae8ff"];
const palette = [...p1, ...p2, ...p3, ...p4, ...p5, ...p6, ...p7, ...p8];
$.component(
"c-colorpicker",
(props, { emit }) => {
const handleSelect = (c) => {
if (typeof props.color === "function") props.color(c);
emit("select", c);
};
const getColor = () => props.color() ?? "#000000";
return html`
<div class="card bg-base-200 border-base-300 w-fit border p-2 shadow-sm select-none">
<div class="grid grid-cols-8 gap-0.5">
${() =>
palette.map(
(c) => html`
<button
type="button"
.style=${`background-color: ${c}`}
.class=${() => {
const active = getColor() === c;
return `size-5 rounded-xs cursor-pointer transition-all hover:scale-125 hover:z-10 active:scale-90 outline-none border border-black/5 ${
active ? "ring-2 ring-offset-1 ring-primary z-10 scale-110" : ""
}`;
}}
@click=${() => handleSelect(c)}></button>
`,
)}
</div>
<div class="flex items-center gap-1 mt-2">
<input
type="text"
class="input input-bordered input-xs h-6 px-1 font-mono text-[10px] w-full"
.value=${() => props.color()}
@input=${(e) => handleSelect(e.target.value)} />
<div class="tooltip" data-tip="Copiar">
<button
type="button"
class="btn btn-xs btn-square border border-base-content/20 shadow-inner"
.style=${() => `background-color: ${getColor()}`}
@click=${() => navigator.clipboard.writeText(getColor())}>
<span class="icon-[lucide--copy] text-white mix-blend-difference"></span>
</button>
</div>
</div>
</div>
`;
},
["color"],
);

View File

@@ -0,0 +1,168 @@
import { $, html } from "sigpro";
$.component(
"c-datepicker",
(props, { emit }) => {
const viewDate = $(new Date());
const hoveredDate = $(null);
const todayISO = new Date().toLocaleDateString("en-CA");
const toISOLocal = (date) => {
if (!date) return null;
return date.toISOString().split("T")[0];
};
// Función unificada para navegar tiempo
const navigate = (type, offset) => {
hoveredDate(null);
const d = viewDate();
if (type === "month") {
viewDate(new Date(d.getFullYear(), d.getMonth() + offset, 1));
} else if (type === "year") {
viewDate(new Date(d.getFullYear() + offset, d.getMonth(), 1));
}
};
const selectDate = (dateObj) => {
const isoDate = toISOLocal(dateObj);
const isRange = props.range() === "true" || props.range() === true;
const currentVal = typeof props.value === "function" ? props.value() : props.value;
let result;
if (!isRange) {
result = isoDate;
} else {
const s = currentVal?.start || null;
const e = currentVal?.end || null;
if (!s || (s && e)) {
result = { start: isoDate, end: null };
} else {
result = isoDate < s ? { start: isoDate, end: s } : { start: s, end: isoDate };
}
}
if (typeof props.value === "function") {
props.value(isRange ? { ...result } : result);
}
emit("change", result);
};
const handleGridClick = (e) => {
const btn = e.target.closest("button[data-date]");
if (!btn) return;
selectDate(new Date(btn.getAttribute("data-date")));
};
const days = $(() => {
const d = viewDate();
const year = d.getFullYear();
const month = d.getMonth();
const firstDay = new Date(year, month, 1).getDay();
const offset = firstDay === 0 ? 6 : firstDay - 1;
const total = new Date(year, month + 1, 0).getDate();
let grid = Array(offset).fill(null);
for (let i = 1; i <= total; i++) grid.push(new Date(year, month, i));
return grid;
});
const getWeekNumber = (d) => {
const t = new Date(d.valueOf());
t.setDate(t.getDate() - ((d.getDay() + 6) % 7) + 3);
const firstThurs = t.valueOf();
t.setMonth(0, 1);
if (t.getDay() !== 4) t.setMonth(0, 1 + ((4 - t.getDay() + 7) % 7));
return 1 + Math.ceil((firstThurs - t.getTime()) / 604800000);
};
return html`
<div class="card bg-base-100 shadow-xl border border-base-300 w-80 p-4 pb-6 rounded-box select-none">
<div class="flex justify-between items-center mb-4 gap-1">
<div class="flex gap-0.5">
<button type="button" class="btn btn-ghost btn-xs px-1" @click=${() => navigate("year", -1)}>
<span class="icon-[lucide--chevrons-left] w-4 h-4 opacity-50"></span>
</button>
<button type="button" class="btn btn-ghost btn-xs px-1" @click=${() => navigate("month", -1)}>
<span class="icon-[lucide--chevron-left] w-4 h-4"></span>
</button>
</div>
<span class="text-xs font-bold capitalize flex-1 text-center">
${() => viewDate().toLocaleString("es-ES", { month: "long" }).toUpperCase()}
<span class="opacity-50 ml-1">${() => viewDate().getFullYear()}</span>
</span>
<div class="flex gap-0.5">
<button type="button" class="btn btn-ghost btn-xs px-1" @click=${() => navigate("month", 1)}>
<span class="icon-[lucide--chevron-right] w-4 h-4"></span>
</button>
<button type="button" class="btn btn-ghost btn-xs px-1" @click=${() => navigate("year", 1)}>
<span class="icon-[lucide--chevrons-right] w-4 h-4 opacity-50"></span>
</button>
</div>
</div>
<div class="grid grid-cols-8 gap-1 px-1" @click=${handleGridClick}>
<div class="flex items-center justify-center text-[10px] opacity-40 font-bold uppercase"></div>
${() =>
["L", "M", "X", "J", "V", "S", "D"].map(
(l) => html`
<div class="flex items-center justify-center text-[10px] opacity-40 font-bold uppercase">${l}</div>
`,
)}
${() =>
days().map((date, i) => {
const isFirstCol = i % 7 === 0;
const iso = date ? toISOLocal(date) : null;
const btnClass = () => {
if (!date) return "";
const val = typeof props.value === "function" ? props.value() : props.value;
const isR = props.range() === "true" || props.range() === true;
const sDate = isR ? val?.start : typeof val === "string" ? val : val?.start;
const eDate = isR ? val?.end : null;
const hDate = hoveredDate();
const isSel = iso === sDate || iso === eDate;
const tEnd = eDate || hDate;
const inRange = isR && sDate && tEnd && !isSel && ((iso > sDate && iso < tEnd) || (iso < sDate && iso > tEnd));
return `btn btn-xs p-0 aspect-square min-h-0 h-auto font-normal rounded-md relative
${isSel ? "btn-primary !text-primary-content shadow-md" : "btn-ghost"}
${inRange ? "!bg-primary/20 !text-base-content" : ""}`;
};
return html`
${isFirstCol
? html`
<div class="flex items-center justify-center text-[10px] opacity-30 italic bg-base-200/50 rounded-md aspect-square">
${date ? getWeekNumber(date) : days()[i + 6] ? getWeekNumber(days()[i + 6]) : ""}
</div>
`
: ""}
${date
? html`
<button
type="button"
class="${btnClass}"
data-date="${date.toISOString()}"
@mouseenter=${() => hoveredDate(iso)}
@mouseleave=${() => hoveredDate(null)}>
${iso === todayISO
? html`
<span class="absolute -inset-px border-2 border-primary/60 rounded-md pointer-events-none"></span>
`
: ""}
<span class="relative z-10 pointer-events-none">${date.getDate()}</span>
</button>
`
: html`
<div class="aspect-square"></div>
`}
`;
})}
</div>
</div>
`;
},
["range", "value"],
);

View File

@@ -0,0 +1,37 @@
import { $, html } from "sigpro";
$.component(
"c-dialog",
(props, { slot, emit }) => {
return html`
<dialog
.class=${() => `modal ${props.open() ? "modal-open" : ""}`}
.open=${() => props.open()}
@close=${(e) => {
if (typeof props.open === "function") props.open(false);
emit("close", e);
}}>
<div class="modal-box">
<div class="flex flex-col gap-4">${slot()}</div>
<div class="modal-action">
<form method="dialog" @submit=${() => props.open(false)}>
${slot("buttons")}
${() =>
!slot("buttons").length
? html`
<button class="btn">Cerrar</button>
`
: ""}
</form>
</div>
</div>
<form method="dialog" class="modal-backdrop" @submit=${() => props.open(false)}>
<button>close</button>
</form>
</dialog>
`;
},
["open"],
);

View File

@@ -0,0 +1,31 @@
import { $, html } from "sigpro";
$.component(
"c-drawer",
(props, { emit, slot }) => {
const id = `drawer-${Math.random().toString(36).substring(2, 9)}`;
return html`
<div class="drawer">
<input
id="${id}"
type="checkbox"
class="drawer-toggle"
.checked=${() => props.open()}
@change=${(e) => {
const isChecked = e.target.checked;
if (typeof props.open === "function") props.open(isChecked);
emit("change", isChecked);
}} />
<div class="drawer-content">${slot("content")}</div>
<div class="drawer-side z-999">
<label for="${id}" aria-label="close sidebar" class="drawer-overlay"></label>
<div class="bg-base-200 min-h-full w-80">${slot()}</div>
</div>
</div>
`;
},
["open"],
);

View File

@@ -0,0 +1,20 @@
import { $, html } from "sigpro";
$.component(
"c-dropdown",
(props, { slot }) => {
// Generamos un ID único para el anclaje nativo
const id = props.id() ?? `pop-${Math.random().toString(36).slice(2, 7)}`;
return html`
<div class="inline-block">
<button class="btn" popovertarget="${id}" style="anchor-name: --${id}">${slot("trigger")}</button>
<div popover id="${id}" style="position-anchor: --${id}" class="dropdown menu bg-base-100 rounded-box shadow-sm border border-base-300">
${slot()}
</div>
</div>
`;
},
["id"],
);

View File

@@ -0,0 +1,37 @@
import { $, html } from "sigpro";
$.component(
"c-fab",
(props, { emit }) => {
const handleClick = (e, item) => {
if (item.onclick) item.onclick(e);
emit("select", item);
if (document.activeElement instanceof HTMLElement) document.activeElement.blur();
};
return html`
<div class="dropdown dropdown-top dropdown-end fixed bottom-6 right-6 z-100">
<div tabindex="0" role="button" .class=${() => `btn btn-lg btn-circle btn-primary shadow-2xl ${props.ui() ?? ""}`}>
<span class="${() => props["main-icon"]() || "icon-[lucide--plus]"} w-6 h-6"></span>
</div>
<ul tabindex="0" class="dropdown-content menu mb-4 p-0 flex flex-col gap-3 items-center">
${() =>
(props.actions() || []).map(
(item) => html`
<li class="p-0">
<button
.class=${() => `btn btn-circle shadow-lg ${item.ui() ?? "btn-secondary"}`}
@click=${(e) => handleClick(e, item)}
.title=${item.label}>
<span class="${item.icon} w-5 h-5"></span>
</button>
</li>
`,
)}
</ul>
</div>
`;
},
["main-icon", "actions", "ui"],
);

View File

@@ -0,0 +1,26 @@
import { $, html } from "sigpro";
$.component(
"c-input",
(props, { slot, emit }) => {
return html`
<div class="${props.tooltip() ? "tooltip" : ""}" data-tip=${() => props.tooltip() ?? ""}>
<label class="floating-label">
<span>${() => props.label() ?? ""}</span>
<label class=${() => `input ${props.ui() ?? ""}`}>
<input
type=${() => props.type() ?? "text"}
class="input"
:value=${props.value}
placeholder=${() => props.place() ?? props.label() ?? ""}
@input=${(e) => emit("input", e.target.value)}
@change=${(e) => emit("change", e.target.value)} />
<span>${slot("icon-action")}</span>
<span class=${() => props.icon() ?? ""}></span>
</label>
</label>
</div>
`;
},
["label", "value", "icon", "tooltip", "ui", "place", "type"],
);

View File

@@ -0,0 +1,46 @@
import { html } from "sigpro";
export const loading = (show = true, msg = "Cargando...") => {
const body = document.body;
if (!show) {
if (loadingEl) {
loadingEl.classList.replace("opacity-100", "opacity-0");
body.style.removeProperty("overflow"); // Restaurar scroll
const elToRemove = loadingEl; // Captura para el closure
elToRemove.addEventListener(
"transitionend",
() => {
if (elToRemove === loadingEl) {
// Solo si sigue siendo el actual
elToRemove.remove();
loadingEl = null;
}
},
{ once: true },
);
}
return;
}
if (loadingEl?.isConnected) {
loadingEl.querySelector(".loading-text").textContent = msg;
return;
}
body.style.overflow = "hidden"; // Bloquear scroll
loadingEl = html`
<div
class="fixed inset-0 z-9999 flex items-center justify-center bg-base-300/40 backdrop-blur-md transition-opacity duration-300 opacity-0 pointer-events-auto select-none">
<div class="flex flex-col items-center gap-4">
<span class="loading loading-spinner loading-lg text-primary"></span>
<span class="loading-text font-bold text-lg text-base-content">${msg}</span>
</div>
</div>
`.firstElementChild;
body.appendChild(loadingEl);
requestAnimationFrame(() => loadingEl.classList.replace("opacity-0", "opacity-100"));
};

View File

@@ -0,0 +1,57 @@
import { $, html } from "sigpro";
$.component(
"c-menu",
(props, { emit }) => {
const getItems = () => props.items() || [];
const renderItems = (data) => {
return data.map((item) => {
const hasChildren = item.sub && item.sub.length > 0;
const content = html`
${item.icon
? html`
<span class="${item.icon} h-4 w-4"></span>
`
: ""}
<span>${item.label}</span>
`;
if (hasChildren) {
return html`
<li>
<details .open="${!!item.open}">
<summary>${content}</summary>
<ul>
${renderItems(item.sub)}
</ul>
</details>
</li>
`;
}
return html`
<li>
<a
href="${item.href || "#"}"
.class=${item.active ? "active" : ""}
@click="${(e) => {
if (!item.href || item.href === "#") e.preventDefault();
if (item.onClick) item.onClick(item);
emit("select", item);
}}">
${content}
</a>
</li>
`;
});
};
return html`
<ul .class=${() => `menu bg-base-200 rounded-box w-full ${props.ui() ?? ""}`}>
${() => renderItems(getItems())}
</ul>
`;
},
["items", "ui"],
);

View File

@@ -0,0 +1,28 @@
import { $, html } from "sigpro";
$.component(
"c-radio",
(props, { emit }) => {
return html`
<label class="label cursor-pointer flex justify-start gap-4">
<input
type="radio"
.name=${() => props.name()}
.value=${() => props.value()}
.class=${() => `radio ${props.ui() ?? ""}`}
.disabled=${() => props.disabled()}
.checked=${() => props.checked()}
@change=${(e) => {
if (e.target.checked) emit("change", props.value());
}} />
${() =>
props.label()
? html`
<span class="label-text">${() => props.label()}</span>
`
: ""}
</label>
`;
},
["checked", "name", "label", "ui", "disabled", "value"],
);

View File

@@ -0,0 +1,24 @@
import { $, html } from "sigpro";
$.component(
"c-range",
(props, { emit }) => {
return html`
<input
type="range"
.min=${() => props.min() ?? 0}
.max=${() => props.max() ?? 100}
.step=${() => props.step() ?? 1}
.value=${() => props.value()}
.class=${() => `range ${props.ui() ?? ""}`}
@input=${(e) => {
const val = e.target.value;
if (typeof props.value === "function") props.value(val);
emit("input", val);
emit("change", val);
}} />
`;
},
["ui", "value", "min", "max", "step"],
);

View File

@@ -0,0 +1,34 @@
import { $, html } from "sigpro";
$.component(
"c-rating",
(props, { emit }) => {
const count = () => parseInt(props.count() ?? 5);
const getVal = () => {
const v = props.value();
return v === false || v == null ? 0 : Number(v);
};
return html`
<div .class=${() => `rating ${props.mask() ?? ""}`}>
${() =>
Array.from({ length: count() }).map((_, i) => {
const radioValue = i + 1;
return html`
<input
type="radio"
.name=${() => props.name()}
.class=${() => `mask ${props.mask() ?? "mask-star"}`}
.checked=${() => getVal() === radioValue}
@change=${() => {
if (typeof props.value === "function") props.value(radioValue);
emit("change", radioValue);
}} />
`;
})}
</div>
`;
},
["value", "count", "name", "mask"],
);

View File

@@ -0,0 +1,31 @@
import { $, html } from "sigpro";
$.component(
"c-tab",
(props, { emit, slot }) => {
const groupName = `tab-group-${Math.random().toString(36).substring(2, 9)}`;
const items = () => props.items() || [];
return html`
<div .class=${() => `tabs ${props.ui() ?? "tabs-lifted"}`}>
${() =>
items().map(
(item) => html`
<input
type="radio"
name="${groupName}"
class="tab"
.checked=${() => props.value() === item.value}
@change=${() => {
if (typeof props.value === "function") props.value(item.value);
emit("change", item.value);
}} />
<label class="tab">${item.label}</label>
`,
)}
</div>
<div class="tab-content bg-base-100 border-base-300 p-6">${() => slot(props.value())}</div>
`;
},
["items", "value", "ui"],
);

View File

@@ -0,0 +1,49 @@
import { html } from "sigpro";
let container = null;
export const toast = (msg, type = "alert-success", ms = 3500) => {
if (!container || !container.isConnected) {
container = document.createElement("div");
container.className = "fixed top-0 right-0 z-9999 p-6 flex flex-col gap-4 pointer-events-none items-end";
document.body.appendChild(container);
}
const close = (n) => {
if (!n || n._c) return;
n._c = 1;
Object.assign(n.style, { transform: "translateX(100%)", opacity: 0 });
setTimeout(() => {
Object.assign(n.style, { maxHeight: "0px", marginBottom: "-1rem", marginTop: "0px", padding: "0px" });
}, 100);
n.addEventListener("transitionend", (e) => {
if (["max-height", "opacity"].includes(e.propertyName)) {
n.remove();
if (!container.hasChildNodes()) (container.remove(), (container = null));
}
});
};
const el = html`
<div
class="card bg-base-100 shadow-xl border border-base-200 w-80 sm:w-96 overflow-hidden transition-all duration-500 ease-in-out transform translate-x-full opacity-0 pointer-events-auto"
style="max-height:200px">
<div class="card-body p-1">
<div role="alert" class="${`alert ${type} alert-soft border-none p-2`}">
<div class="flex items-center justify-between w-full gap-2">
<span class="font-medium text-sm">${msg}</span>
<button class="btn btn-ghost btn-xs btn-circle" @click="${(e) => close(e.target.closest(".card"))}">
<span class="icon-[lucide--circle-x] w-5 h-5"></span>
</button>
</div>
</div>
</div>
</div>
`.firstElementChild;
container.appendChild(el);
requestAnimationFrame(() => requestAnimationFrame(() => el.classList.remove("translate-x-full", "opacity-0")));
setTimeout(() => close(el), ms);
};

View File

@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SigProUI Test</title>
<!-- CSS de la librería UI (se procesa con Vite) -->
<link rel="stylesheet" href="./sigproui.css" />
</head>
<body>
<div id="app"></div>
<script type="module">
import App from './app.js';
import lucide from 'lucide';
// Initialize icons
lucide.createIcons();
// Mount the app
document.getElementById('app').appendChild(App());
</script>
</body>
</html>

View File

@@ -0,0 +1,70 @@
// index.js
import "./sigproui.css";
import "./components/Button.js";
import "./components/Card.js";
import "./components/Checkbox.js";
import "./components/ColorPicker.js";
import "./components/DatePicker.js";
import "./components/Dialog.js";
import "./components/Drawer.js";
import "./components/Dropdown.js";
import "./components/Fab.js";
import "./components/Input.js";
import "./components/Loading.js";
import "./components/Menu.js";
import "./components/Radio.js";
import "./components/Range.js";
import "./components/Rating.js";
import "./components/Tab.js";
import "./components/Toast.js";
export { default as Button } from "./components/Button.js";
export { default as Card } from "./components/Card.js";
export { default as Checkbox } from "./components/Checkbox.js";
export { default as ColorPicker } from "./components/ColorPicker.js";
export { default as DatePicker } from "./components/DatePicker.js";
export { default as Dialog } from "./components/Dialog.js";
export { default as Drawer } from "./components/Drawer.js";
export { default as Dropdown } from "./components/Dropdown.js";
export { default as Fab } from "./components/Fab.js";
export { default as Input } from "./components/Input.js";
export { default as Loading } from "./components/Loading.js";
export { default as Menu } from "./components/Menu.js";
export { default as Radio } from "./components/Radio.js";
export { default as Range } from "./components/Range.js";
export { default as Rating } from "./components/Rating.js";
export { default as Tab } from "./components/Tab.js";
export { default as Toast } from "./components/Toast.js";
export const components = [
"Button",
"Card",
"Checkbox",
"ColorPicker",
"DatePicker",
"Dialog",
"Drawer",
"Dropdown",
"Fab",
"Input",
"Loading",
"Menu",
"Radio",
"Range",
"Rating",
"Tab",
"Toast",
];
// Exportar versión
export const version = "1.0.0";
export const name = "SigProUI";
export default {
version,
name,
description: "Biblioteca de componentes UI basada en SigPro, Tailwind CSS y DaisyUI",
components,
};

View File

@@ -0,0 +1,146 @@
/**
* SigProUI - Estilos de la biblioteca de componentes UI
* Requiere Tailwind CSS y DaisyUI
*/
/* Tailwind + DaisyUI */
@import "tailwindcss";
@plugin "daisyui" {
themes: light --default, dark;
}
/* Utilidades personalizadas de SigProUI */
.btn-ghost {
border-color: transparent !important;
}
.floating-label > span {
font-size: 1.1rem;
}
/* Transiciones para componentes */
.input {
transition: all 0.3s ease-in-out;
outline: none;
appearance: none;
align-items: center;
}
.input:hover {
background-color: var(--color-base-300);
}
/* Indicadores y badges */
.indicator {
position: relative;
}
.indicator-item {
position: absolute;
top: 0;
right: 0;
transform: translate(50%, -50%);
}
/* Tooltips */
.tooltip {
position: relative;
}
.tooltip::after {
content: attr(data-tip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
padding: 0.5rem 1rem;
background: oklch(var(--p));
color: oklch(var(--pc));
border-radius: 0.375rem;
font-size: 0.75rem;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
}
.tooltip:hover::after {
opacity: 1;
}
/* Estados de carga */
.loading {
display: inline-block;
width: 1rem;
height: 1rem;
border: 2px solid currentColor;
border-right-color: transparent;
border-radius: 50%;
animation: spin 0.75s linear infinite;
}
.loading.loading-xs {
width: 1rem;
height: 1rem;
}
.loading.loading-sm {
width: 1rem;
height: 1rem;
}
.loading.loading-md {
width: 1.5rem;
height: 1.5rem;
}
.loading.loading-lg {
width: 2.5rem;
height: 2.5rem;
}
.hidden {
display: none;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Loading spinner variants */
.loading-spinner {
border-right-color: transparent;
}
.loading-dots::after {
content: "";
animation: dots 1s infinite;
}
@keyframes dots {
0%, 20% { content: "."; }
40% { content: ".."; }
60%, 100% { content: "..."; }
}
.loading-ring {
border-bottom-color: transparent;
}
.loading-facebook::after {
content: "";
animation: facebook 1.5s infinite;
}
@keyframes facebook {
0% { transform: scale(0, 0.035); }
25% { transform: scale(0.035, 0.035); }
50% { transform: scale(0.035, 1); }
75% { transform: scale(1, 1); }
100% { transform: scale(1, 0.035); }
}