npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

smartlayout-elsolya

v0.1.4

Published

Framework-agnostic layout rules engine with adapters for React/Vue/Next/Nuxt/Angular/Node

Readme

SmartLayout-elsolya Documentation

Overview

SmartLayout-elsolya is a Rules Engine library for dynamically selecting Layouts based on different conditions such as path, roles, permissions, and flags.

The library is designed to be Framework-agnostic - one Core that works with any environment, with ready-made Adapters for:

  • React
  • Next.js
  • Vue 3
  • Nuxt 3
  • Angular
  • Node.js

Installation

npm install smartlayout-elsolya

Core Concepts

1. Layout Engine

The core engine that contains the rules and uses them to determine the appropriate Layout based on the context.

2. Layout Context

The object that contains current information such as:

  • path: Current path
  • roles: Current user roles
  • permissions: Permissions
  • flags: Custom flags
  • meta: Additional data
  • params, query: URL parameters
  • user: User data

3. Rules

Each rule contains:

  • when: Conditions that must be met
  • use: Layout name or a function that returns a Layout name
  • priority: Priority (optional) - Rules with higher priority are evaluated first

Core API (Framework-agnostic)

Creating a Layout Engine

import { createLayoutEngine } from "smartlayout-elsolya";

const engine = createLayoutEngine({
  defaultLayout: "default", // Default Layout
  rules: [
    // Rule: use "admin" layout for paths starting with /admin/ and users with admin role
    {
      when: { 
        path: "/admin/**", 
        roles: ["admin"] 
      }, 
      use: "admin" 
    },
    
    // Rule: use "auth" layout for all paths starting with /auth/
    {
      when: { 
        path: "/auth/**" 
      }, 
      use: "auth" 
    },
    
    // Rule: use a dynamic function to determine the Layout
    {
      when: { 
        path: "/dashboard/**" 
      }, 
      use: (ctx) => {
        // If there's a flag named beta with value true, use dashboard-beta
        return ctx.flags?.beta ? "dashboard-beta" : "dashboard";
      }
    },
  ],
});

Using the Engine

// Determine the Layout based on context
const layoutName = engine.resolve({
  path: "/admin/users",
  roles: ["admin"]
});
// Result: "admin"

const layoutName2 = engine.resolve({
  path: "/auth/login"
});
// Result: "auth"

Managing Rules Dynamically

// Add new rules
engine.addRules(
  { when: { path: "/blog/**" }, use: "blog" },
  { when: { path: "/shop/**" }, use: "shop" }
);

// Replace all rules
engine.setRules([
  { when: { path: "/new/**" }, use: "new-layout" }
]);

// Get all rules
const rules = engine.getRules();

// Get the default Layout
const defaultLayout = engine.getDefaultLayout();

When Clause Conditions

1. Path

You can use:

  • String patterns: with support for * and **
    • "/admin/**" - any path starting with /admin/
    • "/user/*/profile" - /user/anything/profile
    • "**" - any path
  • RegExp: Regular expression pattern
  • Array: Multiple patterns
{
  when: {
    path: "/admin/**" // or ["/admin/**", "/dashboard/**"] or /^\/admin/
  },
  use: "admin"
}

2. Roles

{
  when: {
    roles: ["admin", "moderator"] // User must have one of these roles
  },
  use: "admin"
}

3. Permissions

{
  when: {
    permissions: ["read:users", "write:users"] // Must have all these permissions
  },
  use: "admin"
}

4. Flags

{
  when: {
    flags: {
      beta: true,
      premium: true
    }
  },
  use: "premium-layout"
}

5. Meta Data

{
  when: {
    meta: {
      requiresAuth: true,
      layout: "custom"
    }
  },
  use: "custom-layout"
}

Combining Conditions

You can combine all conditions together:

{
  when: {
    path: "/admin/**",
    roles: ["admin"],
    permissions: ["manage:users"],
    flags: { beta: true }
  },
  use: "admin-beta"
}

Priority

Rules with higher priority are evaluated first:

const engine = createLayoutEngine({
  defaultLayout: "default",
  rules: [
    {
      when: { path: "/admin/**" },
      use: "admin",
      priority: 10 // High priority
    },
    {
      when: { path: "/**" }, // Matches any path
      use: "default",
      priority: 1 // Low priority
    }
  ]
});

React

Import

import { LayoutProvider, LayoutRenderer, useLayout } from "smartlayout-elsolya/react";
import { createLayoutEngine } from "smartlayout-elsolya";

Complete Example

import React from "react";
import { LayoutProvider, LayoutRenderer } from "smartlayout-elsolya/react";
import { createLayoutEngine } from "smartlayout-elsolya";

// Create the Engine
const engine = createLayoutEngine({
  defaultLayout: "default",
  rules: [
    { when: { path: "/auth/**" }, use: "auth" },
    { when: { path: "/admin/**", roles: ["admin"] }, use: "admin" },
    { when: { path: "/dashboard/**" }, use: "dashboard" },
  ],
});

// Define the Layouts
const layouts = {
  default: ({ children }: { children: React.ReactNode }) => (
    <div className="default-layout">
      <header>Default Header</header>
      <main>{children}</main>
      <footer>Default Footer</footer>
    </div>
  ),
  
  auth: ({ children }: { children: React.ReactNode }) => (
    <div className="auth-layout">
      <div className="auth-container">{children}</div>
    </div>
  ),
  
  admin: ({ children }: { children: React.ReactNode }) => (
    <div className="admin-layout">
      <nav>Admin Navigation</nav>
      <main>{children}</main>
    </div>
  ),
  
  dashboard: ({ children }: { children: React.ReactNode }) => (
    <div className="dashboard-layout">
      <aside>Sidebar</aside>
      <main>{children}</main>
    </div>
  ),
};

// Function to get context
function getContext() {
  return {
    path: window.location.pathname,
    roles: ["admin"], // Can be fetched from state management
    flags: { beta: false },
  };
}

// Main component
export function App() {
  return (
    <LayoutProvider engine={engine} getContext={getContext}>
      <LayoutRenderer layouts={layouts}>
        <div>Page Content</div>
      </LayoutRenderer>
    </LayoutProvider>
  );
}

Using the Hook

import { useLayout } from "smartlayout-elsolya/react";

function MyComponent() {
  const { layout, ctx } = useLayout();
  
  return (
    <div>
      <p>Current Layout: {layout}</p>
      <p>Current Path: {ctx.path}</p>
    </div>
  );
}

With React Router

import { useLocation } from "react-router-dom";

function getContext() {
  const location = useLocation();
  return {
    path: location.pathname,
    roles: getCurrentUserRoles(), // Your custom function
  };
}

Next.js

Import

import { resolveLayoutForNext, useNextLayout } from "smartlayout-elsolya/next";
import { createLayoutEngine } from "smartlayout-elsolya";

Usage in _app.tsx or app/layout.tsx

// app/layout.tsx (App Router)
import { createLayoutEngine } from "smartlayout-elsolya";
import { resolveLayoutForNext } from "smartlayout-elsolya/next";
import { headers } from "next/headers";

const engine = createLayoutEngine({
  defaultLayout: "default",
  rules: [
    { when: { path: "/admin/**" }, use: "admin" },
    { when: { path: "/auth/**" }, use: "auth" },
  ],
});

export default function RootLayout({ children }: { children: React.ReactNode }) {
  const headersList = headers();
  const pathname = headersList.get("x-pathname") || "/";
  
  const layoutName = resolveLayoutForNext(engine, {
    path: pathname,
    roles: getRolesFromCookies(), // Your custom function
  });

  const Layout = layouts[layoutName] || layouts.default;
  
  return <Layout>{children}</Layout>;
}

Using Hook in Client Components

"use client";

import { useNextLayout } from "smartlayout-elsolya/next";
import { usePathname } from "next/navigation";

export function ClientLayout({ children }: { children: React.ReactNode }) {
  const pathname = usePathname();
  const { layout } = useNextLayout(engine, () => ({
    path: pathname,
  }));

  const Layout = layouts[layout] || layouts.default;
  return <Layout>{children}</Layout>;
}

Vue 3

Import

import { createSmartLayoutVuePlugin, useSmartLayout } from "smartlayout-elsolya/vue";
import { createLayoutEngine } from "smartlayout-elsolya";

Plugin Setup

// main.ts
import { createApp } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import { createSmartLayoutVuePlugin } from "smartlayout-elsolya/vue";
import { createLayoutEngine } from "smartlayout-elsolya";
import App from "./App.vue";

const router = createRouter({
  history: createWebHistory(),
  routes: [
    // routes...
  ],
});

const engine = createLayoutEngine({
  defaultLayout: "default",
  rules: [
    { when: { path: "/admin/**" }, use: "admin" },
    { when: { path: "/auth/**" }, use: "auth" },
  ],
});

const app = createApp(App);

app.use(router);
app.use(
  createSmartLayoutVuePlugin({
    engine,
    router,
    getContext: (to) => ({
      path: to.path,
      roles: getCurrentUserRoles(), // Your custom function
    }),
  })
);

app.mount("#app");

Usage in Components

<template>
  <component :is="currentLayout">
    <slot />
  </component>
</template>

<script setup lang="ts">
import { useSmartLayout } from "smartlayout-elsolya/vue";
import { computed } from "vue";

const { layout } = useSmartLayout();

const layouts = {
  default: () => import("./layouts/DefaultLayout.vue"),
  admin: () => import("./layouts/AdminLayout.vue"),
  auth: () => import("./layouts/AuthLayout.vue"),
};

const currentLayout = computed(() => layouts[layout.value] || layouts.default);
</script>

Nuxt 3

Import

import { installNuxtSmartLayout } from "smartlayout-elsolya/nuxt";
import { createLayoutEngine } from "smartlayout-elsolya";

Plugin Setup

// plugins/smartlayout.client.ts
import { installNuxtSmartLayout } from "smartlayout-elsolya/nuxt";
import { createLayoutEngine } from "smartlayout-elsolya";

export default defineNuxtPlugin((nuxtApp) => {
  const engine = createLayoutEngine({
    defaultLayout: "default",
    rules: [
      { when: { path: "/admin/**" }, use: "admin" },
      { when: { path: "/auth/**" }, use: "auth" },
    ],
  });

  installNuxtSmartLayout({
    nuxtApp,
    engine,
    setPageLayout: (name) => {
      setPageLayout(name); // Nuxt function
    },
    getContext: () => ({
      path: useRoute().path,
      roles: getCurrentUserRoles(), // Your custom function
    }),
  });
});

Usage in Pages

<!-- pages/admin/users.vue -->
<template>
  <div>Admin Users Page</div>
</template>

<script setup>
// Layout will be applied automatically based on rules
</script>

Angular

Import

import { SmartLayoutService, SmartLayoutModule } from "smartlayout-elsolya/angular";
import { createLayoutEngine } from "smartlayout-elsolya";

Service Setup

// app.component.ts
import { Component, OnInit } from "@angular/core";
import { Router, NavigationEnd } from "@angular/router";
import { SmartLayoutService } from "smartlayout-elsolya/angular";
import { createLayoutEngine } from "smartlayout-elsolya";

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
})
export class AppComponent implements OnInit {
  constructor(
    private layoutService: SmartLayoutService,
    private router: Router
  ) {
    const engine = createLayoutEngine({
      defaultLayout: "default",
      rules: [
        { when: { path: "/admin/**" }, use: "admin" },
        { when: { path: "/auth/**" }, use: "auth" },
      ],
    });

    this.layoutService.init(engine);
  }

  ngOnInit() {
    this.router.events.subscribe((event) => {
      if (event instanceof NavigationEnd) {
        this.layoutService.setContext({
          path: event.url,
          roles: this.getCurrentUserRoles(), // Your custom function
        });
      }
    });
  }
}

Usage in Template

<!-- app.component.html -->
<ng-container [ngSwitch]="layout$ | async">
  <app-default-layout *ngSwitchCase="'default'">
    <router-outlet></router-outlet>
  </app-default-layout>
  
  <app-admin-layout *ngSwitchCase="'admin'">
    <router-outlet></router-outlet>
  </app-admin-layout>
  
  <app-auth-layout *ngSwitchCase="'auth'">
    <router-outlet></router-outlet>
  </app-auth-layout>
</ng-container>
// app.component.ts
export class AppComponent {
  layout$ = this.layoutService.layout$;
  
  constructor(private layoutService: SmartLayoutService) {}
}

Node.js (Express/Connect)

Import

import { createNodeMiddleware } from "smartlayout-elsolya/node";
import { createLayoutEngine } from "smartlayout-elsolya";

Middleware Usage

import express from "express";
import { createNodeMiddleware } from "smartlayout-elsolya/node";
import { createLayoutEngine } from "smartlayout-elsolya";

const app = express();

const engine = createLayoutEngine({
  defaultLayout: "default",
  rules: [
    { when: { path: "/admin/**" }, use: "admin" },
    { when: { path: "/api/**" }, use: "api" },
  ],
});

// Add Middleware
app.use(
  createNodeMiddleware(engine, (req) => ({
    path: req.path,
    roles: req.user?.roles || [],
  }))
);

// Use req.layoutName in Routes
app.get("/admin/users", (req, res) => {
  const layoutName = req.layoutName; // "admin"
  res.render(layoutName, { /* data */ });
});

app.listen(3000);

Advanced Examples

Example 1: Dynamic Layout Based on Flags

const engine = createLayoutEngine({
  defaultLayout: "default",
  rules: [
    {
      when: { path: "/dashboard/**" },
      use: (ctx) => {
        if (ctx.flags?.newDesign) return "dashboard-v2";
        if (ctx.flags?.beta) return "dashboard-beta";
        return "dashboard";
      },
    },
  ],
});

Example 2: Layout Based on Permissions

const engine = createLayoutEngine({
  defaultLayout: "default",
  rules: [
    {
      when: {
        path: "/admin/**",
        permissions: ["admin:full"],
      },
      use: "admin-full",
      priority: 10,
    },
    {
      when: {
        path: "/admin/**",
        permissions: ["admin:read"],
      },
      use: "admin-readonly",
      priority: 5,
    },
  ],
});

Example 3: Layout Based on Meta Data

const engine = createLayoutEngine({
  defaultLayout: "default",
  rules: [
    {
      when: {
        meta: { layout: "custom" },
      },
      use: (ctx) => ctx.meta?.layout || "default",
    },
  ],
});

Example 4: Adding Rules Dynamically

// In React Component
function AdminPanel() {
  useEffect(() => {
    // Add a temporary rule
    engine.addRules({
      when: { path: "/admin/**", flags: { debug: true } },
      use: "admin-debug",
      priority: 20,
    });

    return () => {
      // Remove the rule on unmount
      engine.setRules(engine.getRules().filter(/* ... */));
    };
  }, []);
}

API Reference

createLayoutEngine(config: LayoutEngineConfig): LayoutEngine

Create a new Layout Engine.

Parameters:

  • config.defaultLayout: Default Layout name
  • config.rules: Array of rules (optional)

Returns: LayoutEngine object

LayoutEngine.resolve(ctx: LayoutContext): LayoutName

Determine the Layout based on context.

Parameters:

  • ctx: LayoutContext object

Returns: Layout name

LayoutEngine.setRules(rules: LayoutRule[]): void

Replace all rules.

LayoutEngine.addRules(...rules: LayoutRule[]): void

Add new rules.

LayoutEngine.getRules(): LayoutRule[]

Get all rules.

LayoutEngine.getDefaultLayout(): LayoutName

Get the default Layout.


TypeScript Types

LayoutContext

type LayoutContext = {
  path: string;
  routeName?: string;
  meta?: Record<string, any>;
  roles?: string[];
  permissions?: string[];
  flags?: Record<string, boolean>;
  params?: Record<string, any>;
  query?: Record<string, any>;
  user?: any;
};

LayoutRule

type LayoutRule = {
  when: WhenClause;
  use: LayoutName | ((ctx: LayoutContext) => LayoutName);
  priority?: number;
};

WhenClause

type WhenClause = {
  path?: string | RegExp | Array<string | RegExp>;
  roles?: string[];
  permissions?: string[];
  flags?: Record<string, boolean>;
  meta?: Record<string, any>;
};

Best Practices

  1. Use Priority wisely: More specific rules should have higher priority
  2. Separate rules by function: Organize rules in separate files
  3. Use dynamic use function for complex conditions: Instead of creating many rules
  4. Keep Layout Context updated: Make sure to update context when path or roles change
  5. Use TypeScript: To help catch errors early

License

MIT License - Ahmed Alsolya


Support and Contribution

For contributions or reporting issues, please open an Issue or Pull Request in the repository.