diff --git a/.storybook/main.ts b/.storybook/main.ts index 04d60238c..aa46e7a91 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -3,10 +3,7 @@ import { mergeConfig } from 'vite' const config: StorybookConfig = { stories: ['../src/**/*.stories.ts'], - addons: [ - '@storybook/addon-links', - '@storybook/addon-docs', - ], + addons: ['@storybook/addon-docs'], framework: '@storybook/web-components-vite', async viteFinal (config) { diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 8d450a6e8..576bf2027 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,4 +1,57 @@ +import { withProvider, getThemeColors } from '@/storybook' +import { definePreview } from '@storybook/web-components-vite' +import addonDocs from '@storybook/addon-docs' + import '@/styles/theme.css' -export const tags = ['autodocs'] -export const parameters = {} +export default definePreview({ + addons: [addonDocs()], + tags: ['autodocs'], + decorators: [withProvider], + parameters: { + options: { + storySort: { + order: ['Basic UI', 'Advanced', 'Solid'], + }, + }, + docs: { + source: { + excludeDecorators: true, + transform(code) { + const MAX_LINE_LENGTH = 120; + const lines = code.trim().split('\n'); + const formattedLines = lines.map(line => { + if (line.length <= MAX_LINE_LENGTH) { + return line; + } + + const baseIndentMatch = line.match(/^\s*/); + const baseIndent = baseIndentMatch ? baseIndentMatch[0] : ''; + const attrIndent = baseIndent + ' '; + const formattedLine = line.replace(/ ([a-z0-9-]+=")/gi, `\n${attrIndent}$1`); + + if (formattedLine !== line) { + return formattedLine.replace(/>(<\/[a-z0-9-]+>)$/i, `\n${baseIndent}>$1`); + } + + return formattedLine; + }); + + return formattedLines.filter(line => line.trim() !== '').join('\n'); + }, + }, + }, + }, + globalTypes: { + primaryColor: { + description: 'Primary Color', + defaultValue: 'purple', + toolbar: { + title: 'Primary', + icon: 'paintbrush', + items: getThemeColors().map((color) => ({ label: color.slice(0, 1).toUpperCase() + color.slice(1), value: color })), + dynamicTitle: true, + }, + }, + }, +}); diff --git a/README.md b/README.md index 42276d2f3..0466aab8b 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,15 @@ import { log } from 'solid-ui' log.msg('Hello there!') ``` +Finally, if you're going to use a component that requires global state (like `` which hooks into authentication), you'll need to wrap your app with ``: + +```html + + + + +``` + ## Use directly in a browser Solid-UI provides **ESM** bundles for direct browser usage. You'll also need to include the CSS styles: diff --git a/package-lock.json b/package-lock.json index c9ad281c2..1a0c7ca07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,7 +39,6 @@ "@iconify/json": "2.2.481", "@rolldown/plugin-babel": "^0.2.3", "@storybook/addon-docs": "10.4.2", - "@storybook/addon-links": "10.4.2", "@storybook/web-components-vite": "10.4.2", "@tailwindcss/vite": "^4.3.0", "@testing-library/dom": "^10.4.1", @@ -4406,33 +4405,6 @@ } } }, - "node_modules/@storybook/addon-links": { - "version": "10.4.2", - "resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.4.2.tgz", - "integrity": "sha512-cU8h4/m+oAr8UUwF4teZG2N1ilV+vU+98Ii/Ma+IIx9M/V7i5544UxfAz84dV5Rx2Oho6x8XH3gIvmevSyPi/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/global": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.4.2" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "react": { - "optional": true - } - } - }, "node_modules/@storybook/builder-vite": { "version": "10.4.2", "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.4.2.tgz", diff --git a/package.json b/package.json index d8d2f5340..0f85fd0f7 100644 --- a/package.json +++ b/package.json @@ -120,7 +120,6 @@ "@iconify/json": "2.2.481", "@rolldown/plugin-babel": "^0.2.3", "@storybook/addon-docs": "10.4.2", - "@storybook/addon-links": "10.4.2", "@storybook/web-components-vite": "10.4.2", "@tailwindcss/vite": "^4.3.0", "@testing-library/dom": "^10.4.1", diff --git a/src/components/account/Account.stories.ts b/src/components/account/Account.stories.ts index 78995baaf..09fec7d5a 100644 --- a/src/components/account/Account.stories.ts +++ b/src/components/account/Account.stories.ts @@ -1,22 +1,21 @@ import { html } from 'lit' -import { USER_OPTIONS, defineAuthStoryRender } from '@/storybook' +import { USER_OPTIONS } from '@/storybook' import './Account' const meta = { - title: 'Account', + title: 'Solid/Account', args: { user: 'Alice', }, argTypes: { user: USER_OPTIONS.control, - } + }, + render: () => html`` } as const -const render = defineAuthStoryRender(() => html``) - -export const Primary = { render } -export const Guest = { render, args: { user: 'Guest' } } -export const Initializing = { render, args: { user: 'Initializing' } } +export const Primary = {} +export const Guest = { args: { user: 'Guest' } } +export const Initializing = { args: { user: 'Initializing' } } export default meta diff --git a/src/components/avatar/Avatar.stories.ts b/src/components/avatar/Avatar.stories.ts index 74c89b743..3cca19133 100644 --- a/src/components/avatar/Avatar.stories.ts +++ b/src/components/avatar/Avatar.stories.ts @@ -1,22 +1,21 @@ import { html } from 'lit' -import { USER_OPTIONS, defineAuthStoryRender } from '@/storybook' +import { USER_OPTIONS } from '@/storybook' import './Avatar' const meta = { - title: 'Avatar', + title: 'Solid/Avatar', args: { user: 'Alice', }, argTypes: { user: USER_OPTIONS.control, - } + }, + render: () => html`` } as const -const render = defineAuthStoryRender(() => html``) - -export const Primary = { render } -export const Fallback = { render, args: { user: 'Bob' } } -export const Guest = { render, args: { user: 'Guest' } } +export const Primary = {} +export const Fallback = { args: { user: 'Bob' } } +export const Guest = { args: { user: 'Guest' } } export default meta diff --git a/src/components/button/Button.stories.ts b/src/components/button/Button.stories.ts index 403a66471..709b6f045 100644 --- a/src/components/button/Button.stories.ts +++ b/src/components/button/Button.stories.ts @@ -1,6 +1,6 @@ import { html, nothing } from 'lit' import { unsafeHTML } from 'lit/directives/unsafe-html.js' -import { defineControlOptions, defineStoryRender } from '@/storybook' +import { defineControlOptions } from '@/storybook' import '~icons/lucide/check' import '~icons/lucide/plus' @@ -17,17 +17,20 @@ const ICON_OPTIONS = defineControlOptions([ ['None', null] ]) +const args = { + text: 'Save Changes', + title: '', + variant: 'primary', + icon: 'None', + leftIcon: 'None', + rightIcon: 'None', + disabled: false, + loading: false, +} as const + const meta = { - title: 'Button', - args: { - text: 'Save Changes', - title: '', - variant: 'primary', - leftIcon: 'None', - rightIcon: 'None', - disabled: false, - loading: false, - }, + title: 'Basic UI/Button', + args, argTypes: { variant: { control: 'select', @@ -41,29 +44,25 @@ const meta = { disabled: { control: 'boolean' }, loading: { control: 'boolean' }, }, -} as const - -const render = defineStoryRender(({ text, title, variant, icon, leftIcon, rightIcon, disabled, loading }) => { - const resolvedIcon = ICON_OPTIONS.resolve(icon) - const resolvedLeftIcon = ICON_OPTIONS.resolve(leftIcon) - const resolvedRightIcon = ICON_OPTIONS.resolve(rightIcon) + render ({ icon, leftIcon, rightIcon, variant, disabled, loading, title, text }: typeof args) { + const resolvedIcon = ICON_OPTIONS.resolve(icon) + const resolvedLeftIcon = ICON_OPTIONS.resolve(leftIcon) + const resolvedRightIcon = ICON_OPTIONS.resolve(rightIcon) - return html` - + return html` + ${resolvedLeftIcon ? unsafeHTML(``) : nothing} ${resolvedIcon ? unsafeHTML(``) : nothing} ${text} ${resolvedRightIcon ? unsafeHTML(``) : nothing} ` -}) - -export default meta + } +} as const -export const Primary = { render } +export const Primary = {} export const Secondary = { - render, args: { text: 'Cancel', variant: 'secondary' @@ -71,7 +70,6 @@ export const Secondary = { } export const Tertiary = { - render, args: { text: 'Add More', variant: 'tertiary', @@ -80,7 +78,6 @@ export const Tertiary = { } export const Outline = { - render, args: { text: 'Sign Up', variant: 'outline', @@ -89,7 +86,6 @@ export const Outline = { } export const Ghost = { - render, args: { text: '', variant: 'ghost', @@ -97,3 +93,5 @@ export const Ghost = { title: 'Open help', }, } + +export default meta diff --git a/src/components/combobox/Combobox.stories.ts b/src/components/combobox/Combobox.stories.ts index 3f790219a..7e5a8fc16 100644 --- a/src/components/combobox/Combobox.stories.ts +++ b/src/components/combobox/Combobox.stories.ts @@ -1,27 +1,10 @@ import { html } from 'lit' -import { defineStoryRender } from '@/storybook' import { defineAsyncComboboxOptionsProvider } from './Combobox' import '@/components/combobox-option' import './Combobox' -const meta = { - title: 'Combobox', - args: { - label: 'What is the best food?', - options: 'Pizza, Ramen, Tacos', - asyncJSOptions: false, - asyncHtmlOptions: false, - }, - argTypes: { - label: { control: 'text' }, - options: { control: 'text' }, - asyncJSOptions: { control: 'boolean' }, - asyncHtmlOptions: { control: 'boolean' }, - }, -} as const - const pokemonProvider = defineAsyncComboboxOptionsProvider(async (query) => { const response = await fetch('https://beta.pokeapi.co/graphql/v1beta', { method: 'POST', @@ -47,8 +30,21 @@ const pokemonProvider = defineAsyncComboboxOptionsProvider(async (query) => { })) }) -const render = defineStoryRender( - ({ label, options, asyncJSOptions, asyncHtmlOptions }) => { +const meta = { + title: 'Basic UI/Combobox', + args: { + label: 'What is the best food?', + options: 'Pizza, Ramen, Tacos', + asyncJSOptions: false, + asyncHtmlOptions: false, + }, + argTypes: { + label: { control: 'text' }, + options: { control: 'text' }, + asyncJSOptions: { control: 'boolean' }, + asyncHtmlOptions: { control: 'boolean' }, + }, + render ({ label, options, asyncJSOptions, asyncHtmlOptions }) { if (asyncJSOptions) { return html`` } @@ -56,7 +52,7 @@ const render = defineStoryRender( if (asyncHtmlOptions) { return html` ( return html` - ${parsedOptions.map((option) => html`${option}`)} + ${parsedOptions.map((option, index) => { + const indent = index === 0 ? '' : ' ' + + return html`${indent}${option}\n` + })} ` } -) - -export default meta +} as const -export const Primary = { render } +export const Primary = {} export const AsyncWithJS = { args: { label: 'Who is the best Pokemon?', asyncJSOptions: true, }, - render, } export const AsyncWithHtml = { @@ -92,5 +89,6 @@ export const AsyncWithHtml = { label: 'Who is the best Disney character?', asyncHtmlOptions: true, }, - render, } + +export default meta diff --git a/src/components/dialog/Dialog.stories.ts b/src/components/dialog/Dialog.stories.ts new file mode 100644 index 000000000..1e91fab36 --- /dev/null +++ b/src/components/dialog/Dialog.stories.ts @@ -0,0 +1,73 @@ +import { html } from 'lit' +import { showDialog } from '@/lib/dialogs' + +import '@/components/button' +import SampleModal from './docs/sample-modal' + +/** + * Solid UI implements a Dialog System that allows to render dialogs dynamically, instead of declaring them inline. + * + * This works using `` to orchestrate the opening and closing of dialogs under the hood. If + * you're using ``, it comes configured out of the box. + */ +const meta = { + title: 'Advanced/Dialogs', + parameters: { + docs: { + source: { + language: 'ts', + code: ` + import { customElement, DialogComponent, showDialog } from 'solid-ui' + import { html } from 'lit' + import { property } from 'lit/decorators.js' + + import 'solid-ui/components/button' + import 'solid-ui/components/dialog' + import 'solid-ui/components/dialog-content' + import 'solid-ui/components/dialog-footer' + + @customElement('solid-ui-sample-modal') + export class SampleModal extends DialogComponent<'red' | 'green' | 'blue'> { + @property({ type: String }) + accessor message: string = '' + + protected render () { + return html\` + + + \${this.message} + + + this.close('red')}>Red + this.close('green')}>Green + this.close('blue')}>Blue + + + \` + } + } + + await showDialog(SampleModal, { + props: { + message: 'Choose your favorite color:', + }, + onClose: (result) => alert(\`You chose \${result || 'none'}!\`), + }); + ` + } + } + }, +} as const + +export const Primary = { + render: () => html` + showDialog(SampleModal, { + props: { + message: 'Choose your favorite color:', + }, + onClose: (result) => alert(`You chose ${result || 'none'}!`), + })}>Open Modal + ` +} + +export default meta diff --git a/src/components/dialog/Dialog.ts b/src/components/dialog/Dialog.ts index d7bcbe009..6b8ed7dea 100644 --- a/src/components/dialog/Dialog.ts +++ b/src/components/dialog/Dialog.ts @@ -60,7 +60,7 @@ export default class Dialog extends WebComponent {

${this.title}

- + this.close()}> Close diff --git a/src/components/dialog/docs/sample-modal/SampleModal.ts b/src/components/dialog/docs/sample-modal/SampleModal.ts new file mode 100644 index 000000000..20930ce00 --- /dev/null +++ b/src/components/dialog/docs/sample-modal/SampleModal.ts @@ -0,0 +1,29 @@ +import { customElement, DialogComponent } from '@/lib/components' +import { html } from 'lit' +import { property } from 'lit/decorators.js' + +import '@/components/button' +import '@/components/dialog' +import '@/components/dialog-content' +import '@/components/dialog-footer' + +@customElement('solid-ui-sample-modal') +export default class SampleModal extends DialogComponent<'red' | 'green' | 'blue'> { + @property({ type: String }) + accessor message: string = '' + + protected render () { + return html` + + + ${this.message} + + + this.close('red')}>Red + this.close('green')}>Green + this.close('blue')}>Blue + + + ` + } +} diff --git a/src/components/dialog/docs/sample-modal/index.ts b/src/components/dialog/docs/sample-modal/index.ts new file mode 100644 index 000000000..913733fdb --- /dev/null +++ b/src/components/dialog/docs/sample-modal/index.ts @@ -0,0 +1,4 @@ +import SampleModal from './SampleModal' + +export { SampleModal } +export default SampleModal diff --git a/src/components/dialogs-root/DialogsRoot.ts b/src/components/dialogs-root/DialogsRoot.ts index 931d970c6..18270d715 100644 --- a/src/components/dialogs-root/DialogsRoot.ts +++ b/src/components/dialogs-root/DialogsRoot.ts @@ -33,11 +33,8 @@ export default class DialogsRoot extends WebComponent { protected render () { return html`${this.dialogs.map(dialog => html` - dialog.setElement(event.detail)} - > - ${dialog.template} + + ${dialog.element} `)}` } diff --git a/src/components/guard/Guard.stories.ts b/src/components/guard/Guard.stories.ts index 3ee42c2a8..5f6d124a1 100644 --- a/src/components/guard/Guard.stories.ts +++ b/src/components/guard/Guard.stories.ts @@ -1,29 +1,29 @@ import { html } from 'lit' -import { defineAuthStoryRender } from '@/storybook' +import { USER_OPTIONS } from '@/storybook/stubs' import './Guard' -import { USER_OPTIONS } from '@/storybook/stubs' const meta = { - title: 'Guard', + title: 'Solid/Guard', args: { user: 'Alice', }, argTypes: { user: USER_OPTIONS.control, + }, + render () { + return html` + + Initializing content + Guest content + Logged in content + + ` } } as const -const render = defineAuthStoryRender(() => html` - - Initializing content - Guest content - Logged in content - -`) - -export const Primary = { render } -export const Guest = { render, args: { user: 'Guest' } } -export const Initializing = { render, args: { user: 'Initializing' } } +export const Primary = {} +export const Guest = { args: { user: 'Guest' } } +export const Initializing = { args: { user: 'Initializing' } } export default meta diff --git a/src/components/input/Input.stories.ts b/src/components/input/Input.stories.ts index cc5bfa265..d07128e39 100644 --- a/src/components/input/Input.stories.ts +++ b/src/components/input/Input.stories.ts @@ -1,16 +1,17 @@ import { html } from 'lit' import './Input' -import { defineStoryRender } from '@/storybook' + +const args = { + label: 'Name', + value: '', + placeholder: 'Enter your name', + type: 'text', +} const meta = { - title: 'Input', - args: { - label: 'Name', - value: '', - placeholder: 'Enter your name', - type: 'text', - }, + title: 'Basic UI/Input', + args, argTypes: { label: { control: 'text' }, value: { control: 'text' }, @@ -20,17 +21,18 @@ const meta = { options: ['text', 'email', 'password', 'search', 'url'], }, }, + render ({ label, value, placeholder, type }: typeof args) { + return html` + + ` + } } as const -const render = defineStoryRender(({ label, value, placeholder, type }) => html` - -`) +export const Primary = {} export default meta - -export const Primary = { render } diff --git a/src/components/login-button/LoginButton.stories.ts b/src/components/login-button/LoginButton.stories.ts index 733ecd8d7..de03c2d60 100644 --- a/src/components/login-button/LoginButton.stories.ts +++ b/src/components/login-button/LoginButton.stories.ts @@ -1,35 +1,34 @@ import { html } from 'lit' -import { defineStoryRender } from '@/storybook' import '~icons/lucide/log-in' import './LoginButton' const meta = { - title: 'LoginButton', + title: 'Solid/LoginButton', } as const export const Primary = { - render: defineStoryRender(() => html``) + render: () => html`` } export const CustomLabel = { - render: defineStoryRender(() => html` + render: () => html` - Sign In - + Sign In + - `) + ` } export const CustomTrigger = { - render: defineStoryRender(() => html` + render: () => html` - + - `) + ` } export default meta diff --git a/src/components/login-modal/LoginModal.stories.ts b/src/components/login-modal/LoginModal.stories.ts index 0460b784a..2737de6d5 100644 --- a/src/components/login-modal/LoginModal.stories.ts +++ b/src/components/login-modal/LoginModal.stories.ts @@ -1,5 +1,5 @@ import { html } from 'lit' -import { defineStoryRender, internals } from '@/storybook' +import { internals } from '@/storybook' import { showDialog } from '@/lib/dialogs' import '@/components/button' @@ -7,35 +7,48 @@ import '@/components/button' import LoginModal from './LoginModal' const meta = { - title: 'Login Modal', + title: 'Solid/Login Modal', + parameters: { + docs: { + source: { + language: 'ts', + code: ` + import { showDialog } from 'solid-ui'; + import LoginModal from 'solid-ui/components/login-modal'; + + await showDialog(html\`\`); + ` + } + } + }, } as const -export default meta - export const Primary = { - render: defineStoryRender(() => html` - showDialog(html``)}>Open - `) + render: () => html` + showDialog(LoginModal)}>Open + ` } export const Loading = { - render: defineStoryRender(() => html` + render: () => html` { - const dialog = await showDialog(html``) + const dialog = showDialog(LoginModal) const dialogInternals = internals(dialog, ['submitting']) dialogInternals.submitting = true }}>Open - `) + ` } export const Failed = { - render: defineStoryRender(() => html` + render: () => html` { - const dialog = await showDialog(html``) + const dialog = showDialog(LoginModal) const dialogInternals = internals(dialog, ['failed']) dialogInternals.failed = true }}>Open - `) + ` } + +export default meta diff --git a/src/components/logout-button/LogoutButton.stories.ts b/src/components/logout-button/LogoutButton.stories.ts index bda4c41e6..2845f8392 100644 --- a/src/components/logout-button/LogoutButton.stories.ts +++ b/src/components/logout-button/LogoutButton.stories.ts @@ -1,35 +1,34 @@ import { html } from 'lit' -import { defineStoryRender } from '@/storybook' import '~icons/lucide/log-out' import './LogoutButton' const meta = { - title: 'LogoutButton', + title: 'Solid/LogoutButton', } as const export const Primary = { - render: defineStoryRender(() => html``) + render: () => html`` } export const CustomLabel = { - render: defineStoryRender(() => html` + render: () => html` Sign Out - `) + ` } export const CustomTrigger = { - render: defineStoryRender(() => html` + render: () => html` - `) + ` } export default meta diff --git a/src/components/menu/Menu.stories.ts b/src/components/menu/Menu.stories.ts index 2f5ebee39..5eca76ae8 100644 --- a/src/components/menu/Menu.stories.ts +++ b/src/components/menu/Menu.stories.ts @@ -1,5 +1,4 @@ import { html } from 'lit' -import { defineStoryRender } from '@/storybook' import type Menu from '@/components/menu/Menu' import type MenuItem from '@/components/menu-item/MenuItem' @@ -8,10 +7,6 @@ import '@/components/menu-item' import './Menu' -const meta = { - title: 'Menu', -} as const - function select (event: Event, message: string) { const selectedItem = event.target as MenuItem const menu = selectedItem.parentElement as Menu @@ -26,18 +21,21 @@ function select (event: Event, message: string) { alert(message) } -const render = defineStoryRender(() => html` - - - Open Menu - +const meta = { + title: 'Basic UI/Menu', + render: () => html` + + + Open Menu + + + select(event, 'Selected One!')}>One + select(event, 'Selected Two!')}>Two + External Link + + `, +} as const - select(event, 'Selected One!')}>One - select(event, 'Selected Two!')}>Two - External Link - -`) +export const Primary = {} export default meta - -export const Primary = { render } diff --git a/src/components/select/Select.stories.ts b/src/components/select/Select.stories.ts index 58a55d2c4..2d5114409 100644 --- a/src/components/select/Select.stories.ts +++ b/src/components/select/Select.stories.ts @@ -1,32 +1,36 @@ import { html } from 'lit' -import { defineStoryRender } from '@/storybook' import '@/components/select-option' import './Select' +const args = { + label: 'What is the best food?', + options: 'Pizza, Ramen, Tacos', +} + const meta = { - title: 'Select', - args: { - label: 'What is the best food?', - options: 'Pizza, Ramen, Tacos' - }, + title: 'Basic UI/Select', + args, argTypes: { label: { control: 'text' }, options: { control: 'text' }, }, -} as const + render ({ label, options }: typeof args) { + const parsedOptions = options.split(',').map(option => option.trim()) -const render = defineStoryRender(({ label, options }) => { - const parsedOptions = options.split(',').map(option => option.trim()) + return html` + + ${parsedOptions.map((option, index) => { + const indent = index === 0 ? '' : ' ' - return html` - - ${parsedOptions.map(option => html`${option}`)} - - ` -}) + return html`${indent}${option}\n` + })} + + ` + } +} as const -export default meta +export const Primary = {} -export const Primary = { render } +export default meta diff --git a/src/components/signup-button/SignupButton.stories.ts b/src/components/signup-button/SignupButton.stories.ts index 4631f1350..2d4461550 100644 --- a/src/components/signup-button/SignupButton.stories.ts +++ b/src/components/signup-button/SignupButton.stories.ts @@ -1,35 +1,34 @@ import { html } from 'lit' -import { defineStoryRender } from '@/storybook' import '~icons/lucide/user-plus' import './SignupButton' const meta = { - title: 'SignupButton', + title: 'Solid/SignupButton', } as const export const Primary = { - render: defineStoryRender(() => html``) + render: () => html`` } export const CustomLabel = { - render: defineStoryRender(() => html` + render: () => html` Register - `) + ` } export const CustomTrigger = { - render: defineStoryRender(() => html` + render: () => html` - `) + ` } export default meta diff --git a/src/lib/auth/SolidAuth.ts b/src/lib/auth/SolidAuth.ts index d96a34865..34881b7d3 100644 --- a/src/lib/auth/SolidAuth.ts +++ b/src/lib/auth/SolidAuth.ts @@ -3,9 +3,8 @@ import ns from '@/lib/ns' import { authn, authSession, solidLogicSingleton } from 'solid-logic' import { AuthContext } from '@/lib/auth' import { showDialog } from '@/lib/dialogs' -import { html } from 'lit' -import '@/components/login-modal' +import LoginModal from '@/components/login-modal' export const DEFAULT_SIGNUP_URL = 'https://solidproject.org/get_a_pod' @@ -68,7 +67,7 @@ export default class SolidAuth implements AuthContext { async login (loginUrl?: string) { if (!loginUrl) { - showDialog(html``) + showDialog(LoginModal) return } diff --git a/src/lib/components/dialog-component/DialogComponent.ts b/src/lib/components/dialog-component/DialogComponent.ts new file mode 100644 index 000000000..adceff4e8 --- /dev/null +++ b/src/lib/components/dialog-component/DialogComponent.ts @@ -0,0 +1,22 @@ +import DialogTrait from '@/lib/components/traits/DialogTrait' +import { dialogContext } from '@/lib/dialogs/context' +import { WebComponent } from '@/lib/components/web-component' +import { DialogContext, DEFAULT_DIALOG_CONTEXT } from '@/lib/dialogs/context' +import { consume } from '@lit/context' + +export default class DialogComponent extends WebComponent<{ dialogResult: T }> { + private dialogTrait: DialogTrait> + + @consume({ context: dialogContext, subscribe: true }) + private accessor context: DialogContext = DEFAULT_DIALOG_CONTEXT; + + constructor () { + super() + + this.dialogTrait = this.addTrait(new DialogTrait(this, { getContext: () => this.context })) + } + + protected close (result?: T) { + this.dialogTrait.close(result) + } +} diff --git a/src/lib/components/dialog-component/index.ts b/src/lib/components/dialog-component/index.ts new file mode 100644 index 000000000..fa573b774 --- /dev/null +++ b/src/lib/components/dialog-component/index.ts @@ -0,0 +1,4 @@ +import DialogComponent from './DialogComponent' + +export { DialogComponent } +export default DialogComponent diff --git a/src/lib/components/index.ts b/src/lib/components/index.ts index 807b1e89b..2ec105c37 100644 --- a/src/lib/components/index.ts +++ b/src/lib/components/index.ts @@ -1,4 +1,5 @@ export * from './decorators' +export * from './dialog-component' export * from './ids' -export * from './web-component' export * from './traits' +export * from './web-component' diff --git a/src/lib/components/traits/DialogTrait.ts b/src/lib/components/traits/DialogTrait.ts index 752feca92..fe2088c6a 100644 --- a/src/lib/components/traits/DialogTrait.ts +++ b/src/lib/components/traits/DialogTrait.ts @@ -1,6 +1,7 @@ import { WebComponent } from '@/lib/components' import { CloseDialogEvent } from '@/lib/dialogs/events/close-dialog' import type { DialogContext } from '@/lib/dialogs/context' +import type { GetDialogResult } from '@/lib/dialogs/helpers' import { WebComponentTrait } from './WebComponentTrait' @@ -17,7 +18,7 @@ export default class DialogTrait implements WebComponentTrait { this.config = config } - close (data?: T) { + close (data?: GetDialogResult) { window.dispatchEvent(new CloseDialogEvent(this.config.getContext().id, data)) } } diff --git a/src/lib/components/web-component/WebComponent.ts b/src/lib/components/web-component/WebComponent.ts index 25cc0415e..cc8471167 100644 --- a/src/lib/components/web-component/WebComponent.ts +++ b/src/lib/components/web-component/WebComponent.ts @@ -3,7 +3,12 @@ import type { WebComponentTrait, WebComponentTraitMethodKey } from '@/lib/compon import styles from './WebComponent.styles.css' -export default abstract class WebComponent extends LitElement { +const metadataMarker = Symbol('WebComponentMetadata') + +export type GetWebComponentMetadata = T extends { [metadataMarker]?: infer TMetadata } ? TMetadata : { nope: keyof T } +export type WebComponentConstructor = { new (): T } + +export default abstract class WebComponent = Record> extends LitElement { static states?: Record protected static finalizeStyles (componentStyles?: CSSResultGroup) { @@ -12,6 +17,8 @@ export default abstract class WebComponent extends LitElement { return [styles, ...elementStyles] } + [metadataMarker]?: T + protected internals?: ElementInternals protected globalListeners: [type: string, listener: EventListener][] = [] protected traits: WebComponentTrait[] = [] diff --git a/src/lib/dialogs/Dialog.ts b/src/lib/dialogs/Dialog.ts index 6c3471530..ab3179d66 100644 --- a/src/lib/dialogs/Dialog.ts +++ b/src/lib/dialogs/Dialog.ts @@ -1,28 +1,18 @@ -import { TemplateResult } from 'lit' import { generateId } from '@/lib/components' -export interface DialogConfig { - onClose?(data?: unknown): void +export interface DialogConfig { + onClose?(data?: T): void } export default class Dialog { public readonly id: string - public readonly template: TemplateResult - public readonly element: Promise + public readonly element: HTMLElement public readonly config: DialogConfig - private _resolveElement!: (element: HTMLElement) => void - constructor (template: TemplateResult, config: DialogConfig = {}) { + constructor (element: HTMLElement, config: DialogConfig = {}) { this.id = generateId() - this.template = template this.config = config - this.element = new Promise(resolve => { - this._resolveElement = resolve - }) - } - - setElement (element: HTMLElement) { - this._resolveElement(element) + this.element = element } closed (data?: unknown) { diff --git a/src/lib/dialogs/helpers.ts b/src/lib/dialogs/helpers.ts index b2b7211e5..891a5d302 100644 --- a/src/lib/dialogs/helpers.ts +++ b/src/lib/dialogs/helpers.ts @@ -1,11 +1,21 @@ -import { TemplateResult } from 'lit' import { ShowDialogEvent } from './events/show-dialog' import Dialog, { DialogConfig } from './Dialog' +import type WebComponent from '@/lib/components/web-component/WebComponent' +import type { GetWebComponentMetadata, WebComponentConstructor } from '@/lib/components/web-component/WebComponent' -export function showDialog (template: TemplateResult, config: DialogConfig = {}): Promise { - const dialog = new Dialog(template, config) +export type GetDialogResult = GetWebComponentMetadata extends { dialogResult: infer TResult } ? TResult : TFallback + +export function showDialog ( + DialogComponent: WebComponentConstructor, + config?: DialogConfig> & { props?: Partial } +): T { + const element = new DialogComponent() + + Object.assign(element, config?.props) + + const dialog = new Dialog(element, config) document.dispatchEvent(new ShowDialogEvent(dialog)) - return dialog.element as Promise + return dialog.element as T } diff --git a/src/storybook/helpers.ts b/src/storybook/helpers.ts index 2606149fe..dd9d7f015 100644 --- a/src/storybook/helpers.ts +++ b/src/storybook/helpers.ts @@ -1,45 +1,83 @@ -import { html, render, TemplateResult } from 'lit' +import { html } from 'lit' import { USER_OPTIONS, users } from './stubs' +import type { Decorator } from '@storybook/web-components-vite' import './components/StorybookProvider' +function defineDecorator (decorator: T) { + return decorator +} + +const COLORS = { + black: { + primary: '#131722', + hover: '#dcdcde', + selected: '#b8b9bd', + }, + purple: { + primary: '#8e54ff', + hover: '#eee5ff', + selected: '#ddccff', + }, + blue: { + primary: '#1e6bff', + hover: '#dde9ff', + selected: '#bcd3ff', + }, + pink: { + primary: '#e9127c', + hover: '#fcdbeb', + selected: '#f8b8d8', + }, + indigo: { + primary: '#6236ff', + hover: '#e7e1ff', + selected: '#d0c3ff', + }, + orange: { + primary: '#ff4e00', + hover: '#ffe4d9', + selected: '#ffcab3', + }, + mint: { + primary: '#00c0b0', + hover: '#d9f6f3', + selected: '#b3ece7', + }, +} + export type ControlOptions = [TLabel, TValue][] export type GetStoryArgs = { [K in keyof T]: T[K] extends { options: ArrayLike } ? TValue : T[K] extends { control: 'text' } ? string : never } -function renderStorybook (content: TemplateResult, user?: ReturnType) { - user ??= USER_OPTIONS.resolve('Guest') - - const container = document.createElement('div') +export const withProvider = defineDecorator((story, context) => { + const { primaryColor } = context.globals + const colors = COLORS[primaryColor] ?? COLORS.purple + const user = USER_OPTIONS.resolve((context.args.user ?? context.parameters.user ?? 'Guest') as keyof typeof users) const attributes = 'initialized' in user ? { initialized: user.initialized } : { webId: user.webId, avatarUrl: user.avatarUrl, initialized: true } - render(html` - - ${content} - - `, container) - - return container -} - -export function defineStoryRender (renderStory: (args: GetStoryArgs) => TemplateResult) { - return (args: GetStoryArgs) => renderStorybook(renderStory(args)) -} + return html` + + ${story()} + + ` +}) -export function defineAuthStoryRender (renderStory: (args: GetStoryArgs) => TemplateResult) { - return (args: GetStoryArgs) => { - const resolvedUser = USER_OPTIONS.resolve(args.user as keyof typeof users) - - return renderStorybook(renderStory(args), resolvedUser) - } +export function getThemeColors (): string[] { + return Object.keys(COLORS) } export function defineControlOptions (options: T) { diff --git a/test/unit/index.test.ts b/test/unit/index.test.ts index 88df5d1e8..307b4c9f4 100644 --- a/test/unit/index.test.ts +++ b/test/unit/index.test.ts @@ -13,6 +13,7 @@ describe('Index', () => { 'DEFAULT_DIALOG_CONTEXT', 'DEFAULT_SIGNUP_URL', 'Dialog', + 'DialogComponent', 'DialogTrait', 'FormControlTrait', 'NoopAuth', diff --git a/vite-config/components.ts b/vite-config/components.ts index e843a7efb..fe98bfa6a 100644 --- a/vite-config/components.ts +++ b/vite-config/components.ts @@ -6,6 +6,7 @@ const projectRoot = resolve(import.meta.dirname, '..') export const litDecoratorPaths = [ 'src/components', 'src/storybook', + 'src/lib/components', ] export const componentsSrcDir = join(projectRoot, 'src/components')