Skip to content

Commit

Permalink
feat(js): Introduce UI (novuhq#5746)
Browse files Browse the repository at this point in the history
* feat(js): the base js sdk package scaffolding

* feat(js): improve the package json exports and tsup config

* feat(js): lazy session initialization and interface fixes

* feat(js): renamed the *.spec.ts to *.test.ts

* feat(js): js sdk feeds module

* feat(js): the base js sdk package scaffolding

* feat(js): set the dist file size limits and run the check after the build

* feat(js): lazy session initialization and interface fixes

* feat(js): renamed the *.spec.ts to *.test.ts

* feat(js): js sdk feeds module

* chore(js): simplified the notification and preference classes

* feat(js): lazy session initialization and interface fixes

* feat(js): the base js sdk package scaffolding

* feat(js): js sdk feeds module

* feat(js): handling the web socket connection and events

* chore(js): removed old spec file

* chore(js): fixed the package building on esm and cjs modules

* feat: ui solid

* revert: novu/js package

* feat: implement solidjs

* refactor: update file name

* refactor: remove unused deps

* refactor: rename app to inbox

* refactor: rename ui to inboxui

* feat: add ui to umd

* feat: update tsup build and removed umd for ui

* refactor: imports and babel

* feat: add tailwind css

* feat: add tailwind

* feat: add vanilla

* feat: add vanilla and fix tailwind build

* chore: generate pnpm lock

* chore: update

* feat: remove emotion and vanilla-extract

* feat: integrate appearance and variables into ui

* feat: add ui demo

* fix: tailwind classes

* fix: Scope tailwind styles

* fix: js package build

* refactor: Cleanup css generation

* feat(js): Create variable set

* feat(js): Support multiple descriptors per element

* chore: update lock

* refactor: remove deleted files

* chore: update lock file

* chore: update lock file

* fix: build

* refactor: remove js package from web app

* fix(js): Generate and expose default color css variables

* feat(js): Auto enable darkmode when detected

* fix(js): Revert dark mode support

* chore(js): Remove tailwind-merge dep

* chore(js): Use camelcase for components

* fix(js): Avoid keeping old rules when regenerating

* chore: update lock

* chore: update lock

* chore: update lock

* fix: cspell

---------

Co-authored-by: Paweł <[email protected]>
Co-authored-by: George Desipris <[email protected]>
Co-authored-by: desiprisg <[email protected]>
  • Loading branch information
4 people authored Jun 25, 2024
1 parent 79e319a commit e173dbd
Show file tree
Hide file tree
Showing 18 changed files with 2,577 additions and 276 deletions.
5 changes: 4 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -644,7 +644,10 @@
"Nimma",
"jpath",
"vstack",
"liquidjs"
"liquidjs",
"tailwindcss",
"focusable",
"textareas"
],
"flagWords": [],
"patterns": [
Expand Down
2 changes: 1 addition & 1 deletion packages/js/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module.exports = {
extends: ['../../.eslintrc.js'],
rules: {
'@typescript-eslint/explicit-module-boundary-types': 'error',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/naming-convention': [
'error',
{
Expand Down
22 changes: 20 additions & 2 deletions packages/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"require": "./dist/ui/index.js",
"import": "./dist/ui/index.mjs",
"default": "./dist/ui/index.mjs"
}
},
"./ui/index.css": "./dist/ui/index.css"
},
"files": [
"dist/cjs",
Expand All @@ -38,16 +39,32 @@
"lint": "eslint --ext .ts,.tsx src",
"test": "jest"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/jest": "^29.2.3",
"@types/node": "^18.11.12",
"autoprefixer": "^10.4.0",
"bytes-iec": "^3.1.1",
"chalk": "^5.3.0",
"compression-webpack-plugin": "^10.0.0",
"esbuild-plugin-compress": "^1.0.1",
"esbuild-plugin-solid": "^0.6.0",
"jest": "^29.3.1",
"postcss": "^8.4.38",
"postcss-prefix-selector": "^1.16.1",
"postcss-preset-env": "^9.5.14",
"solid-devtools": "^0.29.2",
"tailwindcss": "^3.4.4",
"terser-webpack-plugin": "^5.3.9",
"tiny-glob": "^0.2.9",
"ts-jest": "^29.0.3",
Expand All @@ -61,6 +78,7 @@
},
"dependencies": {
"@novu/client": "workspace:*",
"clsx": "^2.1.1",
"mitt": "^3.0.1",
"socket.io-client": "4.7.2",
"solid-js": "^1.8.11"
Expand Down
16 changes: 16 additions & 0 deletions packages/js/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module.exports = {
plugins: {
autoprefixer: {},
tailwindcss: {},
'postcss-prefix-selector': {
transform: function (_, selector) {
// Prefix each class selector with :where(.class)
if (selector.startsWith('.')) {
return `:where(${selector})`;
}

return selector; // Return other selectors unchanged
},
},
},
};
50 changes: 35 additions & 15 deletions packages/js/src/ui/Inbox.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { For, createSignal, onMount, type Component } from 'solid-js';
import { For, createSignal, onMount } from 'solid-js';
import { Notification } from '../feeds';
import { Novu } from '../novu';
import type { NovuOptions } from '../novu';
import { Appearance, AppearanceProvider } from './context';
import { useStyle } from './helpers';

const Inbox: Component<{
type InboxProps = {
name: string;
options: NovuOptions;
}> = (props) => {
appearance?: Appearance;
};

export const Inbox = (props: InboxProps) => {
const [feeds, setFeeds] = createSignal<Notification[]>([]);

onMount(() => {
Expand All @@ -19,18 +24,33 @@ const Inbox: Component<{
});

return (
<div>
<header>Hello {props.name} </header>
<For each={feeds()}>
{(feed) => (
<div>
<h2>{feed.body}</h2>
<p>{feed.createdAt}</p>
</div>
)}
</For>
</div>
<AppearanceProvider elements={props.appearance?.elements} variables={props.appearance?.variables}>
<InternalInbox feeds={feeds()} />
</AppearanceProvider>
);
};

export default Inbox;
type InternalInboxProps = {
feeds: Notification[];
};

const InternalInbox = (props: InternalInboxProps) => {
const style = useStyle();

return (
<div class={style('novu', 'root')}>
<div class="nt-bg-primary-500 nt-p-3 nt-m-4">
<div class="nt-text-2xl nt-font-bold">Inbox</div>
<button class={style('nt-bg-red-500', 'button')}>test</button>
<For each={props.feeds}>
{(feed) => (
<div>
<h2>{feed.body}</h2>
<p>{feed.createdAt}</p>
</div>
)}
</For>
</div>
</div>
);
};
1 change: 1 addition & 0 deletions packages/js/src/ui/config/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const NOVU_CSS_IN_JS_STYLESHEET_ID = 'novu-css-in-js-appearance-styles';
13 changes: 13 additions & 0 deletions packages/js/src/ui/config/default-appearance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Variables } from '../context';

export const defaultVariables: Required<Variables> = {
colorPrimary: '#0081F1',
colorPrimaryForeground: 'white',
colorSecondary: '#F3F3F3',
colorSecondaryForeground: '#1A1523',
colorBackground: '#FFFFFF',
colorForeground: '#1A1523',
colorNeutral: 'black',
fontSize: 'inherit',
borderRadius: '0.375rem',
};
2 changes: 2 additions & 0 deletions packages/js/src/ui/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './constants';
export * from './default-appearance';
118 changes: 118 additions & 0 deletions packages/js/src/ui/context/AppearanceContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { ParentProps, createContext, createEffect, createSignal, onMount, useContext } from 'solid-js';
import { createStore } from 'solid-js/store';
import { NOVU_CSS_IN_JS_STYLESHEET_ID, defaultVariables } from '../config';
import { parseElements, parseVariables } from '../helpers';

export type CSSProperties = {
[key: string]: string | number;
};

export type ElementStyles = string | CSSProperties;

export type Elements = {
button?: ElementStyles;
root?: ElementStyles;
};

export type Variables = {
colorBackground?: string;
colorForeground?: string;
colorPrimary?: string;
colorPrimaryForeground?: string;
colorSecondary?: string;
colorSecondaryForeground?: string;
colorNeutral?: string;
fontSize?: string;
borderRadius?: string;
};

export type AppearanceContextType = {
variables?: Variables;
elements?: Elements;
descriptorToCssInJsClass: Record<string, string>;
};

const AppearanceContext = createContext<AppearanceContextType | undefined>(undefined);

export type Appearance = Pick<AppearanceContextType, 'elements' | 'variables'>;

type AppearanceProviderProps = ParentProps & Appearance;

export const AppearanceProvider = (props: AppearanceProviderProps) => {
const [store, setStore] = createStore<{
descriptorToCssInJsClass: Record<string, string>;
}>({ descriptorToCssInJsClass: {} });
const [styleElement, setStyleElement] = createSignal<HTMLStyleElement | null>(null);
const [elementRules, setElementRules] = createSignal<string[]>([]);
const [variableRules, setVariableRules] = createSignal<string[]>([]);

//place style element on HEAD. Placing in body is available for HTML 5.2 onward.
onMount(() => {
const styleEl = document.createElement('style');
styleEl.id = NOVU_CSS_IN_JS_STYLESHEET_ID;
document.head.appendChild(styleEl);

setStyleElement(styleEl);
});

//handle variables
createEffect(() => {
const styleEl = styleElement();

if (!styleEl) {
return;
}

setVariableRules(parseVariables({ ...defaultVariables, ...(props.variables || ({} as Variables)) }));
});

//handle elements
createEffect(() => {
const styleEl = styleElement();

if (!styleEl) {
return;
}

const elementsStyleData = parseElements(props.elements || {});
setStore('descriptorToCssInJsClass', (obj) => ({
...obj,
...elementsStyleData.reduce<Record<string, string>>((acc, item) => {
acc[item.key] = item.className;

return acc;
}, {}),
}));
setElementRules(elementsStyleData.map((el) => el.rule));
});

//add rules to style element
createEffect(() => {
const styleEl = styleElement();
if (!styleEl) {
return;
}

styleEl.innerHTML = [...variableRules(), ...elementRules()].join(' ');
});

return (
<AppearanceContext.Provider
value={{
elements: props.elements || {},
descriptorToCssInJsClass: store.descriptorToCssInJsClass,
}}
>
{props.children}
</AppearanceContext.Provider>
);
};

export function useAppearance() {
const context = useContext(AppearanceContext);
if (!context) {
throw new Error('useAppearance must be used within an AppearanceProvider');
}

return context;
}
1 change: 1 addition & 0 deletions packages/js/src/ui/context/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './AppearanceContext';
2 changes: 2 additions & 0 deletions packages/js/src/ui/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './use-style';
export * from './utils';
32 changes: 32 additions & 0 deletions packages/js/src/ui/helpers/use-style.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { createMemo, createSignal, onMount } from 'solid-js';
import { Elements, useAppearance } from '../context';
import { cn } from './utils';

export const useStyle = () => {
const appearance = useAppearance();
const [isServer, setIsServer] = createSignal(true);

onMount(() => {
setIsServer(false);
});

const styleFuncMemo = createMemo(() => (className: string, descriptor?: keyof Elements | keyof Elements[]) => {
const appearanceClassname =
descriptor && typeof appearance.elements?.[descriptor] === 'string'
? (appearance.elements?.[descriptor] as string) || ''
: '';

const descriptors = (Array.isArray(descriptor) ? descriptor : [descriptor]).map((des) => `nv-${des}`);
const cssInJsClasses =
!!descriptors.length && !isServer() ? descriptors.map((des) => appearance.descriptorToCssInJsClass[des]) : [];

return cn(
...descriptors,
className, // default styles
appearanceClassname, // overrides via appearance prop classes
...cssInJsClasses
);
});

return styleFuncMemo();
};
Loading

0 comments on commit e173dbd

Please sign in to comment.