Skip to content

Commit

Permalink
chore(noti): migrate to Svelte 5
Browse files Browse the repository at this point in the history
  • Loading branch information
vnphanquang committed May 30, 2024
1 parent 6b85cc2 commit 796ff16
Show file tree
Hide file tree
Showing 21 changed files with 524 additions and 683 deletions.
5 changes: 5 additions & 0 deletions .changeset/wet-meals-suffer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@svelte-put/noti': major
---

drop support for Svelte 4 and below. Now using runes for fine-grained reactivity. See [docs page](https://svelte-put.vnphanquang.com/docs/noti) for more information
13 changes: 1 addition & 12 deletions packages/noti/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,6 @@
".": {
"types": "./types/index.d.ts",
"import": "./src/index.js"
},
"./Notification.svelte": {
"types": "./src/Notification.svelte.d.ts",
"svelte": "./src/Notification.svelte"
}
},
"typesVersions": {
"*": {
"Notification.svelte": [
"./src/Notification.svelte.d.ts"
]
}
},
"publishConfig": {
Expand Down Expand Up @@ -68,7 +57,7 @@
"@internals/tsconfig": "workspace:*"
},
"peerDependencies": {
"svelte": "^3.55.0 || ^4.0.0"
"svelte": "^5.0.0-next.1"
},
"volta": {
"extends": "../../package.json"
Expand Down
11 changes: 0 additions & 11 deletions packages/noti/src/Notification.svelte

This file was deleted.

9 changes: 0 additions & 9 deletions packages/noti/src/Notification.svelte.d.ts

This file was deleted.

8 changes: 3 additions & 5 deletions packages/noti/src/errors.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
export class NotFoundVariantConfig extends Error {
/**
* @param {string} variant
* @param {import('./store').NotificationStoreBuilder} builder
* @param {string[]} variants
*/
constructor(variant, builder) {
constructor(variant, variants) {
super(
`No config found for variant '${variant}'. Available variants: ${Object.keys(
builder.variantConfigMap,
).join(', ')}`,
`No config found for variant '${variant}'. Available variants: ${variants.join(', ')}`,
);
}
}
Expand Down
6 changes: 3 additions & 3 deletions packages/noti/src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Copyright (c) Quang Phan. All rights reserved. Licensed under the MIT license.

export { portal } from './portal.js';
export { store } from './store.js';
export { default as Notification } from './Notification.svelte';
export { controller, NotificationControllerBuilder } from './notification-controller-builder.js'
export { Notification } from './notification.svelte.js';
export { NotificationController } from './notification-controller.svelte.js';
61 changes: 61 additions & 0 deletions packages/noti/src/notification-controller-builder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { NotificationController } from './notification-controller.svelte.js';

/**
* @template {Record<string, import('svelte').SvelteComponent<any>>} [VariantMap={}]
*/
export class NotificationControllerBuilder {
/** @type {Record<string, import('./types').NotificationVariantConfig<any, any, any>>} */
#variantConfigMap = {};

/**
* @type {import('./types').NotificationCommonConfig<any, any, any> | undefined}
*/
#init;

/**
* @param {import('./types').NotificationCommonConfig<any, any, any>} [init]
*/
constructor(init) {
this.#init = init;
}

/**
* add config for a notification variant
* @template Resolved
* @template {string} Variant
* @template {import('svelte').SvelteComponent<import('./types').NotificationProps<Resolved>>} Component
* @param {Variant} variant
* @param {import('svelte').ComponentType<Component> | Omit<import('./types').NotificationVariantConfig<Resolved, Variant, Component>, 'variant'>} config
* @returns {NotificationControllerBuilder<VariantMap & Record<Variant, Component>> }
*/
addVariant(variant, config) {
if ('component' in config) {
this.#variantConfigMap[variant] = /** @type {any} */ ({
...config,
variant,
});
} else {
this.#variantConfigMap[variant] = /** @type {any} */ ({
component: config,
variant,
});
}
return this;
}

/**
* Build the actual notification store
* @returns {NotificationController<VariantMap>}
*/
build() {
return new NotificationController(/** @type {any} */(this.#variantConfigMap), this.#init);
}
}

/**
* @param {import('./types').NotificationCommonConfig<any, any, any>} [init]
* @returns {NotificationControllerBuilder}
*/
export function controller(init) {
return new NotificationControllerBuilder(init);
}
195 changes: 195 additions & 0 deletions packages/noti/src/notification-controller.svelte.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { mount } from 'svelte';

import { MissingComponentInCustomPush, NotFoundVariantConfig } from './errors.js';
import { Notification } from './notification.svelte.js';

/**
* @template {Record<string, import('svelte').SvelteComponent>} [VariantMap={}]
*/
export class NotificationController {
/** @type {Record<string, import('./types.js').NotificationVariantConfig<any, any, any>>} */
#variantConfigMap = {};
#counter = 0;

/**
* the notification stack
* @type {Notification<any>[]}
*/
// eslint-disable-next-line no-undef
notifications = $state([]);

/**
* @type {Required<import('./types.js').NotificationCommonConfig<any, any, any>>}
*/
// eslint-disable-next-line no-undef
config = $state({
id: 'uuid',
timeout: 3000,
});

actions = {
/**
* register the element to render a notification into
* @param {HTMLElement} node
* @param {Notification<any>} notification
* @returns {import('./types.js').NotificationPortalActionReturn}
*/
render: (node, notification) => {
mount(notification.config.component, {
target: node,
props: {
...notification.config.props,
notification,
},
intro: true
});
return {};
},
};

/**
* @param {Record<keyof VariantMap, import('./types.js').NotificationVariantConfig<any, any, any>>} variantConfigMap
* @param {import('./types.js').NotificationCommonConfig<any, any, any>} [init]
*/
constructor(variantConfigMap, init) {
if (init?.id) this.config.id = init.id;
if (init?.timeout) this.config.timeout = init.timeout;
this.#variantConfigMap = variantConfigMap;
}


/**
* @template {Extract<keyof VariantMap, string>} Variant
* @template {VariantMap[Variant]} [Component=VariantMap[Variant]]
* @template [Resolved=undefined|Awaited<import('svelte').ComponentProps<Component>['notification']['resolution']>]
* @overload
* @param {Variant} variant
* @param {import('./types.js').NotificationByVariantPushConfig<Resolved, Variant, Component>} [config]
* @returns {Notification<Resolved>}
*/
/**
* @template {import('svelte').SvelteComponent} Component
* @template [Resolved=undefined|Awaited<import('svelte').ComponentProps<Component>['notification']['resolution']>]
* @overload
* @param {'custom'} variant
* @param {import('./types.js').NotificationCustomPushConfig<Resolved, Component>} config
* @returns {Notification<Resolved>}
*/
/**
* @param {string} variant
* @param {import('./types.js').NotificationByVariantPushConfig<any, string, import('svelte').SvelteComponent<any>> | import('./types.js').NotificationCustomPushConfig<any, import('svelte').SvelteComponent<any>>} [config]
* @returns {Notification<any>}
*/
push(variant, config) {
// STEP 1: resolve instance config, merge with common config and variant config, if any
/** @type {import('./types.js').NotificationInstanceConfig<any, any, any>} */
let instanceConfig;
/** @type {NonNullable<import('./types.js').NotificationCommonConfig<Resolved, string, import('svelte').SvelteComponent<any>>['id']>} */
let idResolver;

if (variant === 'custom') {
const rConfig =
/** @type {import('./types.js').NotificationCustomPushConfig<any, any>} */ (
config
);
if (!rConfig || !rConfig.component) {
throw new MissingComponentInCustomPush();
}
instanceConfig = {
...this.config,
...rConfig,
variant: 'custom',
component: rConfig.component,
props: rConfig.props ?? {},
id: '',
};
idResolver = /** @type {any} */ (rConfig.id) ?? this.config.id;
} else {
const variantConfig = this.#variantConfigMap[variant];
if (!variantConfig) {
throw new NotFoundVariantConfig(variant, Object.keys(this.#variantConfigMap));
}
instanceConfig = {
...this.config,
...variantConfig,
...config,
props: {
...variantConfig.props,
...config?.props,
},
id: '',
};
idResolver = /** @type {any} */ (config?.id) ?? variantConfig.id ?? this.config.id;
}

// STEP 2: resolve id for the notification
if (idResolver === 'counter') {
instanceConfig.id = (++this.#counter).toString();
} else if (idResolver === 'uuid') {
instanceConfig.id =
'crypto' in window && crypto.randomUUID
? crypto.randomUUID()
: (++this.#counter).toString();
} else {
instanceConfig.id = idResolver(instanceConfig);
}

// STEP 4: preparing the `Notification` instance
/** @type {Notification<any>} */
let pushed = new Notification(instanceConfig);
pushed.resolution.then(() => {
this.notifications = this.notifications.filter((n) => n.config.id !== pushed.config.id);
});

// STEP 5: push to store
this.notifications.push(pushed);

// STEP 6: start timer if any
pushed.resume();

return pushed;
}

/**
* @overload
* @param {string} [id]
* @param {any} [detail]
* @returns {void}
*/
/**
* @overload
* @param {import('./types.js').NotificationPopVerboseInput} [config]
* @returns {void}
*/
/**
*
* @param {string | import('./types.js').NotificationPopVerboseInput} [config]
* @param {any} [resolved]
* @returns {void}
*/
pop(config, resolved) {
/** @type {string | undefined} */
let id = undefined;

if (config) {
if (typeof config === 'string') {
id = config;
} else {
({ id, resolved } = config);
}
}

/** @type {Notification<any> | undefined} */
let pushed;
if (id) {
pushed = this.notifications.find((n) => n.config.id === id);
} else {
pushed = this.notifications.at(-1);
}

if (pushed) {
pushed.resolve(resolved);
this.notifications = this.notifications.filter((n) => n.config.id !== pushed.config.id);
}
}
}
Loading

0 comments on commit 796ff16

Please sign in to comment.