diff --git a/.changeset/config.json b/.changeset/config.json index aba9d6f..68c9a8b 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -7,5 +7,5 @@ "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", - "ignore": ["ui-playground", "docs"] + "ignore": ["docs", "build-scripts"] } diff --git a/.changeset/large-bikes-behave.md b/.changeset/large-bikes-behave.md new file mode 100644 index 0000000..2a2ca1f --- /dev/null +++ b/.changeset/large-bikes-behave.md @@ -0,0 +1,9 @@ +--- +"@studiocms/ui": minor +--- + +Implement Build step and migrate all exported components into virtual modules + +Instead of `@studiocms/ui/components` use `studiocms:ui/components` + +For more information see https://ui.studiocms.dev \ No newline at end of file diff --git a/.changeset/slimy-ladybugs-join.md b/.changeset/slimy-ladybugs-join.md new file mode 100644 index 0000000..29fc524 --- /dev/null +++ b/.changeset/slimy-ladybugs-join.md @@ -0,0 +1,19 @@ +--- +"@studiocms/ui": minor +--- + +Add a few new components: + +- Accordion +- Badge +- Breadcrumbs +- Group +- Progress +- Sidebar + +Add two new colors + +- `info` (Blue) +- `monochrome` (Black/White) + +Add the ability to pass a CSS file for customization of all colors diff --git a/.github/workflows/ci-pr-snapshot.yml b/.github/workflows/ci-pr-snapshot.yml index 8e10e90..e62a021 100644 --- a/.github/workflows/ci-pr-snapshot.yml +++ b/.github/workflows/ci-pr-snapshot.yml @@ -15,6 +15,9 @@ jobs: - name: Install Tools & Dependencies uses: withstudiocms/automations/.github/actions/install@main + + - name: Build packages + run: pnpm ci:prepublish - name: Publish packages run: pnpm ci:snapshot diff --git a/.github/workflows/ci-push-main.yml b/.github/workflows/ci-push-main.yml index 45b5253..43ab592 100644 --- a/.github/workflows/ci-push-main.yml +++ b/.github/workflows/ci-push-main.yml @@ -53,6 +53,9 @@ jobs: - name: Install Tools & Dependencies uses: withstudiocms/automations/.github/actions/install@main + - name: Build packages + run: pnpm ci:prepublish + - name: Create Release Pull Request or Publish to npm id: changesets uses: changesets/action@v1 diff --git a/.vscode/settings.json b/.vscode/settings.json index 4ff71a7..5d642ce 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,21 +8,27 @@ "editor.gotoLocation.multipleDefinitions": "goto", "cSpell.words": [ "astrojs", + "checkmark", "createtoast", "dismissable", "Eleventy", "fira", "fontsource", "frontmatter", + "heroicons", "iconify", "louisescher", "Matthiesen", "onest", "pathe", + "polyline", + "searchselect", "socialproof", "studiocms", + "themetoggle", "Thum", "tsconfigs", + "vite", "withstudiocms" ] } diff --git a/biome.json b/biome.json index cd36124..3561b54 100644 --- a/biome.json +++ b/biome.json @@ -33,7 +33,13 @@ "rules": { "recommended": true, "suspicious": { - "noExplicitAny": "warn" + "noExplicitAny": "warn", + "noImplicitAnyLet": "info" + }, + "style": { + "noNonNullAssertion": "off", + "noParameterAssign": "info", + "useSingleVarDeclarator": "info" } } }, diff --git a/build-scripts/cmd/build.js b/build-scripts/cmd/build.js new file mode 100644 index 0000000..9579052 --- /dev/null +++ b/build-scripts/cmd/build.js @@ -0,0 +1,187 @@ +import { execSync } from 'node:child_process'; +import fs from 'node:fs/promises'; +import esbuild from 'esbuild'; +import { copy } from 'esbuild-plugin-copy'; +import glob from 'fast-glob'; +import { dim, gray, green, red, yellow } from 'kleur/colors'; + +/** @type {import('esbuild').BuildOptions} */ +const defaultConfig = { + minify: false, + format: 'esm', + platform: 'node', + target: 'node18', + sourcemap: false, + sourcesContent: false, +}; + +const dt = new Intl.DateTimeFormat('en-us', { + hour: '2-digit', + minute: '2-digit', +}); + +const dtsGen = (buildTsConfig) => ({ + name: 'TypeScriptDeclarationsPlugin', + setup(build) { + build.onEnd((result) => { + if (result.errors.length > 0) return; + const date = dt.format(new Date()); + console.log(`${dim(`[${date}]`)} Generating TypeScript declarations...`); + try { + const res = execSync( + `tsc --emitDeclarationOnly ${buildTsConfig ? '-p tsconfig.build.json' : ''} --outDir ./dist` + ); + console.log(res.toString()); + console.log(dim(`[${date}] `) + green('√ Generated TypeScript declarations')); + } catch (error) { + console.error(dim(`[${date}] `) + red(`${error}\n\n${error.stdout.toString()}`)); + } + }); + }, +}); + +const CopyConfig = { + assets: [ + { + from: ['./src/**/*.!(ts|js|css)'], + to: '.', + }, + ], +}; + +export default async function build(...args) { + const config = Object.assign({}, defaultConfig); + const isDev = args.slice(-1)[0] === 'IS_DEV'; + const patterns = args + .filter((f) => !!f) // remove empty args + .map((f) => f.replace(/^'/, '').replace(/'$/, '')); // Needed for Windows: glob strings contain surrounding string chars??? remove these + const entryPoints = [].concat( + ...(await Promise.all( + patterns.map((pattern) => glob(pattern, { filesOnly: true, absolute: true })) + )) + ); + const date = dt.format(new Date()); + + const noClean = args.includes('--no-clean-dist'); + const bundle = args.includes('--bundle'); + const forceCJS = args.includes('--force-cjs'); + const buildTsConfig = args.includes('--build-tsconfig'); + + const { type = 'module', dependencies = {} } = await readPackageJSON('./package.json'); + + config.define = {}; + for (const [key, value] of await getDefinedEntries()) { + config.define[`process.env.${key}`] = JSON.stringify(value); + } + const format = type === 'module' && !forceCJS ? 'esm' : 'cjs'; + + const outdir = 'dist'; + + if (!noClean) { + console.log( + `${dim(`[${date}]`)} Cleaning dist directory... ${dim(`(${entryPoints.length} files found)`)}` + ); + await clean(outdir); + } + + if (!isDev) { + console.log( + `${dim(`[${date}]`)} Building...${bundle ? '(Bundling)' : ''} ${dim(`(${entryPoints.length} files found)`)}` + ); + await esbuild.build({ + ...config, + bundle, + external: bundle ? Object.keys(dependencies) : undefined, + entryPoints, + outdir, + outExtension: forceCJS ? { '.js': '.cjs' } : {}, + format, + plugins: [copy(CopyConfig), dtsGen(buildTsConfig)], + }); + console.log(dim(`[${date}] `) + green('√ Build Complete')); + return; + } + + const rebuildPlugin = { + name: 'dev:rebuild', + setup(build) { + build.onEnd(async (result) => { + const date = dt.format(new Date()); + if (result?.errors.length) { + console.error(dim(`[${date}] `) + red(error || result.errors.join('\n'))); + } else { + if (result.warnings.length) { + console.info( + dim(`[${date}] `) + yellow(`! updated with warnings:\n${result.warnings.join('\n')}`) + ); + } + console.info(dim(`[${date}] `) + green('√ updated')); + } + }); + }, + }; + + const builder = await esbuild.context({ + ...config, + entryPoints, + outdir, + format, + sourcemap: 'linked', + plugins: [copy(CopyConfig), rebuildPlugin, dtsGen(buildTsConfig)], + }); + + console.log( + `${dim(`[${date}] `) + gray('Watching for changes...')} ${dim(`(${entryPoints.length} files found)`)}` + ); + await builder.watch(); + + process.on('beforeExit', () => { + builder.stop?.(); + }); +} + +async function clean(outdir) { + const files = await glob([`${outdir}/**`], { filesOnly: true }); + await Promise.all(files.map((file) => fs.rm(file, { force: true }))); +} + +/** + * Contextual `define` values to statically replace in the built JS output. + * Available to all packages, but mostly useful for CLIs like `create-astro`. + */ +async function getDefinedEntries() { + const define = { + /** The current version (at the time of building) for the current package, such as `astro` or `@astrojs/sitemap` */ + PACKAGE_VERSION: await getInternalPackageVersion('./package.json'), + /** The current version (at the time of building) for `typescript` */ + TYPESCRIPT_VERSION: await getWorkspacePackageVersion('typescript'), + }; + for (const [key, value] of Object.entries(define)) { + if (value === undefined) { + delete define[key]; + } + } + return Object.entries(define); +} + +async function readPackageJSON(path) { + return await fs.readFile(path, { encoding: 'utf8' }).then((res) => JSON.parse(res)); +} + +async function getInternalPackageVersion(path) { + return readPackageJSON(path).then((res) => res.version); +} + +async function getWorkspacePackageVersion(packageName) { + const { dependencies, devDependencies } = await readPackageJSON( + new URL('../../package.json', import.meta.url) + ); + const deps = { ...dependencies, ...devDependencies }; + const version = deps[packageName]; + if (!version) { + throw new Error( + `Unable to resolve "${packageName}". Is it a dependency of the workspace root?` + ); + } + return version.replace(/^\D+/, ''); +} diff --git a/build-scripts/index.js b/build-scripts/index.js new file mode 100755 index 0000000..17a4d6e --- /dev/null +++ b/build-scripts/index.js @@ -0,0 +1,14 @@ +#!/usr/bin/env node +export default async function run() { + const [cmd, ...args] = process.argv.slice(2); + switch (cmd) { + case 'dev': + case 'build': { + const { default: build } = await import('./cmd/build.js'); + await build(...args, cmd === 'dev' ? 'IS_DEV' : undefined); + break; + } + } +} + +run(); diff --git a/build-scripts/jsconfig.json b/build-scripts/jsconfig.json new file mode 100644 index 0000000..5cf3835 --- /dev/null +++ b/build-scripts/jsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "declaration": true, + "strict": true, + "module": "esnext", + "moduleResolution": "node", + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "target": "esnext" + } +} diff --git a/build-scripts/package.json b/build-scripts/package.json new file mode 100644 index 0000000..862fda8 --- /dev/null +++ b/build-scripts/package.json @@ -0,0 +1,19 @@ +{ + "name": "build-scripts", + "version": "0.0.14", + "private": true, + "type": "module", + "main": "./index.js", + "bin": { + "build-scripts": "./index.js" + }, + "dependencies": { + "esbuild": "^0.24.2", + "esbuild-plugin-copy": "^2.1.1", + "fast-glob": "^3.3.2", + "kleur": "^4.1.5", + "p-limit": "^6.1.0", + "tinyexec": "^0.3.1", + "tsconfck": "^3.1.4" + } +} \ No newline at end of file diff --git a/docs/astro.config.mts b/docs/astro.config.mts index 28de2e6..cdf4295 100644 --- a/docs/astro.config.mts +++ b/docs/astro.config.mts @@ -1,5 +1,6 @@ import starlight from '@astrojs/starlight'; import onestWoff2 from '@fontsource-variable/onest/files/onest-latin-wght-normal.woff2?url'; +import ui from '@studiocms/ui'; import { defineConfig, envField } from 'astro/config'; import rehypePluginKit from './src/plugins/rehypePluginKit'; @@ -90,14 +91,14 @@ export default defineConfig({ tag: 'meta', attrs: { property: 'og:image', - content: `${site}og.png?v=2`, + content: `${site}og.png`, }, }, { tag: 'meta', attrs: { property: 'twitter:image', - content: `${site}og.png?v=2`, + content: `${site}og.png`, }, }, { @@ -158,10 +159,6 @@ export default defineConfig({ { label: 'Installation', link: 'docs/', - badge: { - text: 'Updated!', - variant: 'success', - }, }, { label: 'Release Notes', @@ -174,7 +171,14 @@ export default defineConfig({ ], }, { - label: 'Upgrade Guides', + label: 'Guides', + autogenerate: { + directory: 'docs/guides', + collapsed: true, + }, + }, + { + label: 'Upgrading StudioCMS', autogenerate: { directory: 'docs/upgrade-guides', collapsed: true, @@ -196,5 +200,6 @@ export default defineConfig({ }, ], }), + ui() ], }); diff --git a/docs/public/og.png b/docs/public/og.png index 3378612..ea7a695 100644 Binary files a/docs/public/og.png and b/docs/public/og.png differ diff --git a/docs/src/components/DropdownScript.astro b/docs/src/components/DropdownScript.astro index b3a1cb5..b866c51 100644 --- a/docs/src/components/DropdownScript.astro +++ b/docs/src/components/DropdownScript.astro @@ -1,5 +1,5 @@ diff --git a/docs/src/components/ThemeHelperScript.astro b/docs/src/components/ThemeHelperScript.astro index dde1575..5829b54 100644 --- a/docs/src/components/ThemeHelperScript.astro +++ b/docs/src/components/ThemeHelperScript.astro @@ -1,5 +1,5 @@ diff --git a/docs/src/components/landing/EcosystemSection.astro b/docs/src/components/landing/EcosystemSection.astro index 58c4189..076e927 100644 --- a/docs/src/components/landing/EcosystemSection.astro +++ b/docs/src/components/landing/EcosystemSection.astro @@ -1,5 +1,5 @@ --- -import Icon from '@studiocms/ui/utils/Icon.astro'; +import { Icon } from 'studiocms:ui/components'; import AstroLogo from '~/assets/astro.svg'; import LogoAdaptive from '~/assets/logo-adaptive.svg'; import SwordsIcon from '~/assets/swords.svg'; diff --git a/docs/src/components/landing/FormExample.astro b/docs/src/components/landing/FormExample.astro index e9bb822..81e6ce2 100644 --- a/docs/src/components/landing/FormExample.astro +++ b/docs/src/components/landing/FormExample.astro @@ -1,5 +1,5 @@ --- -import { Button, Card, Checkbox, Input } from '@studiocms/ui/components'; +import { Button, Card, Checkbox, Input } from 'studiocms:ui/components'; --- diff --git a/docs/src/components/landing/HeroSection.astro b/docs/src/components/landing/HeroSection.astro index af6005f..36f82e7 100644 --- a/docs/src/components/landing/HeroSection.astro +++ b/docs/src/components/landing/HeroSection.astro @@ -4,6 +4,7 @@ import { Checkbox, Divider, Dropdown, + Icon, Input, RadioGroup, SearchSelect, @@ -12,9 +13,8 @@ import { ThemeToggle, Toggle, User, -} from '@studiocms/ui/components'; +} from 'studiocms:ui/components'; -import Icon from '@studiocms/ui/utils/Icon.astro'; import GitHubIcon from '~/components/icons/GitHubIcon.astro'; ---
@@ -43,7 +43,7 @@ import GitHubIcon from '~/components/icons/GitHubIcon.astro'; Modal @@ -129,7 +129,7 @@ import GitHubIcon from '~/components/icons/GitHubIcon.astro'; Trigger Toast diff --git a/docs/src/components/landing/ModalExample.astro b/docs/src/components/landing/ModalExample.astro index 3564af5..e2cefb8 100644 --- a/docs/src/components/landing/ModalExample.astro +++ b/docs/src/components/landing/ModalExample.astro @@ -1,5 +1,5 @@ --- -import { Button, Modal } from '@studiocms/ui/components'; +import { Button, Modal } from 'studiocms:ui/components'; --- +``` + +It is up to you to create a toggle component. It should be shown once the sidebar can be hidden, the breakpoint being `840px`. + +The helper exposes a few helpful methods to interact with the sidebar: + +```ts title="SingleSidebarHelperExample.ts" +import { SingleSidebarHelper } from 'studiocms:ui/components'; + +const sidebar = new SingleSidebarHelper('my-toggle'); + +// Adds an event listener to the given element +// (specified with its ID) that hides the sidebar +// when clicked. Only applicable on mobile viewports. +sidebar.hideSidebarOnClick('my-element'); + +// Adds an event listener to the given element +// (specified with its ID) that shows the sidebar +// when clicked. Only applicable on mobile viewports. +sidebar.showSidebarOnClick('my-element'); + +// Hides the sidebar. +sidebar.hideSidebar(); + +// Shows the sidebar. +sidebar.showSidebar(); +``` + +### Double Sidebar + +A more complex version with two panes, both with different breakpoints. + +```astro title="DoubleSidebarExample.astro" +--- +import { DoubleSidebar } from 'studiocms:ui/components'; +--- + + +
+ {/* Your outer sidebar content goes here */} +
+
+ {/* Your inner sidebar content goes here */} +
+
+ +``` + +Due to the more complex nature of the double sidebar, it's best to have the following setup: + +- On screens larger than `1200px`, don't show any toggles and show the sidebar entirely. +- On screens smaller than `1200px` and larger than `840px`, show only one toggle to switch between inner and outer sidebar. +- On screens smaller than `840px`, display a toggle to show the outer navbar when it's hidden and a toggle to switch between outer and inner when it's shown. + +To help you with managing the sidebar state, the following methods are exposed from the helper: + +```ts title="DoubleSidebarHelperExample.astro" +import { DoubleSidebarHelper } from 'studiocms:ui/components'; + +const sidebar = new DoubleSidebarHelper(); + +// Register an element that will hide the sidebar entirely when clicked +sidebar.hideSidebarOnClick('my-element'); + +// Register an element that will show the outer sidebar when clicked +sidebar.showOuterOnClick('my-element'); + +// Register an element that will show the inner sidebar when clicked +sidebar.showInnerOnClick('my-element'); + +// Register an element that will toggle between the inner and outer sidebar when clicked +sidebar.toggleStateOnClick('my-element'); + +// All other functions should be self-explanatory: +sidebar.showInnerSidebar(); +sidebar.showOuterSidebar(); +sidebar.toggleSidebarState(); +sidebar.hideSidebar(); +``` \ No newline at end of file diff --git a/docs/src/content/docs/docs/components/accordion.mdx b/docs/src/content/docs/docs/components/accordion.mdx new file mode 100644 index 0000000..ccb1389 --- /dev/null +++ b/docs/src/content/docs/docs/components/accordion.mdx @@ -0,0 +1,139 @@ +--- +title: Accordion +sidebar: + badge: + text: New! + variant: success +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; +import PreviewCard from '~/components/PreviewCard.astro'; +import { Accordion, AccordionItem } from 'studiocms:ui/components'; + +An accordion component. Used to create small pockets of additional information. + +## Usage + + + + + + +
This is the summary
+
These are the details
+
+ +
This is the 2nd summary
+
These are the 2nd details
+
+
+
+
+ + ```astro showLinenumbers title="AccordionExample.astro" + --- + import { Accordion, AccordionItem } from 'studiocms:ui/components'; + --- + + + +
This is the summary
+
These are the details
+
+ +
This is the 2nd summary
+
These are the 2nd details
+
+
+ ``` +
+
+ +### Opening an item by default + +You can pass the `open` prop to any `` to have it be open by default, as seen above in the example. + +### Styles + +The accordion component has multiple different styles: + +- `outlined` (Default) +- `separated` +- `filled` +- `blank` + +They can be used via the `style` prop on the accordion parent. Here's an accordion with the `filled` style: + + + + + + +
This is the summary
+
These are the details
+
+ +
This is the 2nd summary
+
These are the 2nd details
+
+
+
+
+ + ```astro showLinenumbers title="AccordionStyleExample.astro" + --- + import { Accordion, AccordionItem } from 'studiocms:ui/components'; + --- + + + +
This is the summary
+
These are the details
+
+ +
This is the 2nd summary
+
These are the 2nd details
+
+
+ ``` +
+
+ +### Multiple Open Items + +By default, users can open multiple items in the accordion at once. You can change this behavior by setting the `multipleOpen` prop on the `` element to false: + + + + + + +
This is the summary
+
These are the details
+
+ +
This is the 2nd summary
+
These are the 2nd details
+
+
+
+
+ + ```astro showLinenumbers title="AccordionStyleExample.astro" + --- + import { Accordion, AccordionItem } from 'studiocms:ui/components'; + --- + + + +
This is the summary
+
These are the details
+
+ +
This is the 2nd summary
+
These are the 2nd details
+
+
+ ``` +
+
diff --git a/docs/src/content/docs/docs/components/badge.mdx b/docs/src/content/docs/docs/components/badge.mdx new file mode 100644 index 0000000..3261a37 --- /dev/null +++ b/docs/src/content/docs/docs/components/badge.mdx @@ -0,0 +1,132 @@ +--- +title: Badge +sidebar: + badge: + text: New! + variant: success +--- + +import { Badge } from 'studiocms:ui/components'; +import { Tabs, TabItem } from '@astrojs/starlight/components'; +import PreviewCard from '~/components/PreviewCard.astro'; + +A button component with support for different colors, sizes and states out of the box. You can also customize it to your needs! + +## Usage + + + + + + + + + ```astro showLinenumbers title="BadgeExample.astro" + --- + import { Badge } from 'studiocms:ui/components'; + --- + + + ``` + + + +### Colors + +The badge component has support for the following colors: + + + + + + + + + + + + + + ```astro showLinenumbers title="BadgeColorExample.astro" + --- + import { Badge } from 'studiocms:ui/components'; + --- + + + + + + + + ``` + + + +### Variants + + + + + + + + + + + ```astro showLinenumbers title="BadgeSizeExample.astro" + --- + import { Badge } from 'studiocms:ui/components'; + --- + + + + + ``` + + + +### Rounding + +You can change the badge's border-radius by setting the `rounding` prop: + + + + + + + + + + ```astro showLinenumbers title="BadgeSizeExample.astro" + --- + import { Badge } from 'studiocms:ui/components'; + --- + + + + ``` + + + +### Sizes + + + + + + + + + + + ```astro showLinenumbers title="BadgeSizeExample.astro" + --- + import { Badge } from 'studiocms:ui/components'; + --- + + + + + ``` + + diff --git a/docs/src/content/docs/docs/components/breadcrumbs.mdx b/docs/src/content/docs/docs/components/breadcrumbs.mdx new file mode 100644 index 0000000..d38a149 --- /dev/null +++ b/docs/src/content/docs/docs/components/breadcrumbs.mdx @@ -0,0 +1,83 @@ +--- +title: Breadcrumbs +sidebar: + badge: + text: New! + variant: success +--- + +import { Breadcrumbs } from 'studiocms:ui/components'; +import { Tabs, TabItem } from '@astrojs/starlight/components'; +import PreviewCard from '~/components/PreviewCard.astro'; + +A simple breadcrumb generator component! Used to indicate the current page's location within a navigational hierarchy. + +## Usage + + + + + + + + + ```astro showLinenumbers title="ButtonExample.astro" + --- + import { Breadcrumbs } from 'studiocms:ui/components'; + --- + + + ``` + + + +### Changing the separator + +You can change the separator by passing a `separator` prop. + + + + + + + + + ```astro showLinenumbers title="ButtonExample.astro" + --- + import { Breadcrumbs } from 'studiocms:ui/components'; + --- + + + ``` + + diff --git a/docs/src/content/docs/docs/components/button.mdx b/docs/src/content/docs/docs/components/button.mdx index cd7cbbf..4a71714 100644 --- a/docs/src/content/docs/docs/components/button.mdx +++ b/docs/src/content/docs/docs/components/button.mdx @@ -1,9 +1,13 @@ --- i18nReady: true title: Button +sidebar: + badge: + text: New Colors! + variant: tip --- -import { Button } from '@studiocms/ui/components'; +import { Button } from 'studiocms:ui/components'; import { Tabs, TabItem } from '@astrojs/starlight/components'; import PreviewCard from '~/components/PreviewCard.astro'; @@ -11,8 +15,6 @@ A button component with support for different colors, sizes and states out of th ## Usage -Using the button component is easy. Check out the example below: - @@ -24,7 +26,7 @@ Using the button component is easy. Check out the example below: ```astro showLinenumbers title="ButtonExample.astro" --- - import { Button } from '@studiocms/ui/components'; + import { Button } from 'studiocms:ui/components'; --- + + ```astro /color='[a-z]+'/ showLinenumbers title="ButtonColorsExample.astro" --- - import { Button } from '@studiocms/ui/components'; + import { Button } from 'studiocms:ui/components'; --- + + ``` @@ -171,7 +187,7 @@ In addition to colors, the button component can be displayed in three different ```astro /variant=\"[a-z]+\"/ showLinenumbers title="ButtonVariantsExample.astro" --- - import { Button } from '@studiocms/ui/components'; + import { Button } from 'studiocms:ui/components'; --- + + + + + + + ```astro showLinenumbers title="GroupExample.astro" + --- + import { Group, Button } from 'studiocms:ui/components'; + --- + + + + + + + ``` + + diff --git a/docs/src/content/docs/docs/components/input.mdx b/docs/src/content/docs/docs/components/input.mdx index 7366724..a90f3e2 100644 --- a/docs/src/content/docs/docs/components/input.mdx +++ b/docs/src/content/docs/docs/components/input.mdx @@ -5,7 +5,7 @@ title: Input import { Tabs, TabItem } from '@astrojs/starlight/components'; import PreviewCard from '~/components/PreviewCard.astro'; -import { Input } from '@studiocms/ui/components'; +import { Input } from 'studiocms:ui/components'; A simple input component with support for easy labels and placeholders. @@ -20,7 +20,7 @@ A simple input component with support for easy labels and placeholders. ```astro showLinenumbers title="InputExample.astro" --- - import { Input } from '@studiocms/ui/components'; + import { Input } from 'studiocms:ui/components'; --- @@ -41,7 +41,7 @@ You can set the input's type to like you would normally. Here's a password input ```astro "type='password'" showLinenumbers title="PasswordInputExample.astro" --- - import { Input } from '@studiocms/ui/components'; + import { Input } from 'studiocms:ui/components'; --- @@ -62,7 +62,7 @@ You can disable the input altogether by using the `disabled` prop. ```astro "disabled" showLinenumbers title="DisabledInputExample.astro" --- - import { Input } from '@studiocms/ui/components'; + import { Input } from 'studiocms:ui/components'; --- @@ -86,7 +86,7 @@ the `FormData` returned by a submission. Here's an example showing how to set bo ```astro "isRequired" "name='example-input'" showLinenumbers title="FormInputExample.astro" --- - import { Input } from '@studiocms/ui/components'; + import { Input } from 'studiocms:ui/components'; --- diff --git a/docs/src/content/docs/docs/components/modal.mdx b/docs/src/content/docs/docs/components/modal.mdx index 0c8ae2c..abb3c7e 100644 --- a/docs/src/content/docs/docs/components/modal.mdx +++ b/docs/src/content/docs/docs/components/modal.mdx @@ -6,7 +6,7 @@ title: Modal import { Tabs, TabItem } from '@astrojs/starlight/components'; import PreviewCard from '~/components/PreviewCard.astro'; import ModalScript from '~/components/ModalScript.astro'; -import { Modal, Button, Row } from '@studiocms/ui/components'; +import { Modal, Button, Row } from 'studiocms:ui/components'; :::caution[Helper required] This component requires a **helper**. Make sure to read the documentation carefully so you know how to use it. @@ -31,7 +31,7 @@ A dropdown component that takes care of all the annoying bits for you, with adde ```astro showLinenumbers title="ModalExample.astro" --- - import { Modal, Button } from '@studiocms/ui/components'; + import { Modal, Button } from 'studiocms:ui/components'; --- @@ -43,7 +43,8 @@ A dropdown component that takes care of all the annoying bits for you, with adde ``` ```ts twoslash showLinenumbers title="Script Tag" - import { ModalHelper } from '@studiocms/ui/components'; + // @noErrors + import { ModalHelper } from 'studiocms:ui/components'; const modal = new ModalHelper('modal', 'modal-trigger'); ``` @@ -98,7 +99,7 @@ You can change the modal's size to one of three presets. ```astro /size='[a-z]+'/ showLinenumbers title="ModalSizesExample.astro" --- - import { Modal, Button } from '@studiocms/ui/components'; + import { Modal, Button } from 'studiocms:ui/components'; --- @@ -116,7 +117,8 @@ You can change the modal's size to one of three presets. ``` ```ts twoslash showLinenumbers title="Script Tag" - import { ModalHelper } from '@studiocms/ui/components'; + // @noErrors + import { ModalHelper } from 'studiocms:ui/components'; const modal = new ModalHelper('modal', 'modal-trigger'); ``` @@ -147,7 +149,7 @@ You can change whether a modal can be dismissed by clicking outside of it via th ```astro "dismissable={false}" showLinenumbers title="ModalDismissableExample.astro" --- - import { Modal, Button } from '@studiocms/ui/components'; + import { Modal, Button } from 'studiocms:ui/components'; --- @@ -163,7 +165,8 @@ You can change whether a modal can be dismissed by clicking outside of it via th ``` ```ts twoslash showLinenumbers title="Script Tag" - import { ModalHelper } from '@studiocms/ui/components'; + // @noErrors + import { ModalHelper } from 'studiocms:ui/components'; const modal = new ModalHelper('modal', 'modal-trigger'); ``` @@ -196,7 +199,7 @@ Modals support both a "cancel" and a "confirm" button. ```astro "cancelButton='Cancel' actionButton='Confirm'" showLinenumbers title="ModalButtonsExample.astro" --- - import { Modal, Button } from '@studiocms/ui/components'; + import { Modal, Button } from 'studiocms:ui/components'; --- @@ -213,7 +216,7 @@ Modals support both a "cancel" and a "confirm" button. ``` ```ts twoslash showLinenumbers title="Script Tag" // @noErrors - import { ModalHelper } from '@studiocms/ui/components'; + import { ModalHelper } from 'studiocms:ui/components'; const modal = new ModalHelper('modal', 'modal-trigger'); ``` @@ -225,7 +228,7 @@ and `color` keys defined to change the way the buttons look: ```astro "cancelButton={{ label: 'Cancel', color: 'default' }} actionButton={{ label: 'Confirm', color: 'danger' }}" showLinenumbers title="ModalButtonsExample.astro" --- -import { Modal, Button } from '@studiocms/ui/components'; +import { Modal, Button } from 'studiocms:ui/components'; --- + + + + + + + ```astro showLinenumbers title="ProgressExample.astro" + --- + import { Progress } from 'studiocms:ui/components'; + --- + + + ``` + + + +### Value and Max + +Similar to the native HTML `` element, the `Progress` component takes two props: `value` and `max`. The `value` prop represents the current progress, while the `max` prop represents the maximum value of the progress bar. + +The value and max can be adjusted after the component has been initialized by using the `ProgressHelper`, which takes in the ID of the progress bar as the only argument: + + + + + + + + + ```astro showLinenumbers title="ProgressExample.astro" + --- + import { Progress } from 'studiocms:ui/components'; + --- + + + + ``` + + + +### Colors + +The progress component can be colored with all the colors available in the theme by setting the `color` prop to one of the following values: + +- `primary` +- `success` +- `warning` +- `danger` +- `info` +- `monochrome` + + + + + + + + + + + + + + ```astro showLinenumbers title="ProgressExample.astro" + --- + import { Progress } from 'studiocms:ui/components'; + --- + + + + + + + + ``` + + + + diff --git a/docs/src/content/docs/docs/components/radio-group.mdx b/docs/src/content/docs/docs/components/radio-group.mdx index 4cd4f72..de0d4a8 100644 --- a/docs/src/content/docs/docs/components/radio-group.mdx +++ b/docs/src/content/docs/docs/components/radio-group.mdx @@ -1,11 +1,15 @@ --- i18nReady: true title: Radio Group +sidebar: + badge: + text: New Colors! + variant: tip --- import { Tabs, TabItem } from '@astrojs/starlight/components'; import PreviewCard from '~/components/PreviewCard.astro'; -import { RadioGroup, Row } from '@studiocms/ui/components'; +import { RadioGroup, Row } from 'studiocms:ui/components'; A custom radio group component with support for sizes and colors. Form compatible. @@ -20,7 +24,7 @@ A custom radio group component with support for sizes and colors. Form compatibl ```astro showLinenumbers title="RadioGroupExample.astro" --- - import { RadioGroup } from '@studiocms/ui/components'; + import { RadioGroup } from 'studiocms:ui/components'; --- ```astro "horizontal" showLinenumbers title="RadioGroupHorizontalExample.astro" --- - import { RadioGroup } from '@studiocms/ui/components'; + import { RadioGroup } from 'studiocms:ui/components'; --- ```astro "defaultValue='opt-1'" showLinenumbers title="RadioGroupDefaultValueExample.astro" --- - import { RadioGroup } from '@studiocms/ui/components'; + import { RadioGroup } from 'studiocms:ui/components'; --- - - - - - - - - + + + + + + + ```astro "color='primary'" showLinenumbers title="RadioGroupColorExample.astro" --- - import { RadioGroup } from '@studiocms/ui/components'; + import { RadioGroup } from 'studiocms:ui/components'; --- ```astro "disabled: true" showLinenumbers title="RadioGroupDisabledExample.astro" --- - import { RadioGroup } from '@studiocms/ui/components'; + import { RadioGroup } from 'studiocms:ui/components'; --- ```astro showLinenumbers title="RowExample.astro" --- - import { Row } from '@studiocms/ui/components'; + import { Row } from 'studiocms:ui/components'; --- @@ -42,7 +42,7 @@ You can pass the `alignCenter` prop to add the `align-items: center` CSS propert ```astro "alignCenter" showLinenumbers title="RowAlignCenterExample.astro" --- -import { Row } from '@studiocms/ui/components'; +import { Row } from 'studiocms:ui/components'; --- @@ -77,7 +77,7 @@ You can change the gap size of the row via the `gapSize` prop, which can be set ```astro "gapSize='sm'" showLinenumbers title="RowGapSizeExample.astro" --- - import { Row } from '@studiocms/ui/components'; + import { Row } from 'studiocms:ui/components'; --- diff --git a/docs/src/content/docs/docs/components/select-searchable.mdx b/docs/src/content/docs/docs/components/select-searchable.mdx index dc4a93d..873a081 100644 --- a/docs/src/content/docs/docs/components/select-searchable.mdx +++ b/docs/src/content/docs/docs/components/select-searchable.mdx @@ -5,7 +5,7 @@ title: Select (Searchable) import { Tabs, TabItem } from '@astrojs/starlight/components'; import PreviewCard from '~/components/PreviewCard.astro'; -import { SearchSelect } from '@studiocms/ui/components'; +import { SearchSelect } from 'studiocms:ui/components'; A variation of the `` element that supports arrow keys and filtering. ```astro showLinenumbers title="SearchSelectExample.astro" --- - import { SearchSelect } from '@studiocms/ui/components'; + import { SearchSelect } from 'studiocms:ui/components'; --- ```astro "defaultValue='opt-1'" showLinenumbers title="SearchSelectDefaultValueExample.astro" --- - import { SearchSelect } from '@studiocms/ui/components'; + import { SearchSelect } from 'studiocms:ui/components'; --- ```astro "fillWidth" showLinenumbers title="FullWidthSearchSelectExample.astro" --- - import { SearchSelect } from '@studiocms/ui/components'; + import { SearchSelect } from 'studiocms:ui/components'; --- ```astro "placeholder='Custom Placeholder'" showLinenumbers title="SearchSelectPlaceholderExample.astro" --- - import { SearchSelect } from '@studiocms/ui/components'; + import { SearchSelect } from 'studiocms:ui/components'; --- ```astro showLinenumbers title="SelectExample.astro" --- - import { Select } from '@studiocms/ui/components'; + import { Select } from 'studiocms:ui/components'; --- ```astro "fullWidth" showLinenumbers title="SelectFullWidthExample.astro" --- - import { Select } from '@studiocms/ui/components'; + import { Select } from 'studiocms:ui/components'; --- ```astro showLinenumbers title="TabsExample.astro" --- - import { Tabs, TabItem, Card } from '@studiocms/ui/components'; + import { Tabs, TabItem, Card } from 'studiocms:ui/components'; --- @@ -93,7 +93,7 @@ You can add icons and even change the color of the tabs: ```astro /icon='[A-Za-z-]+' color='[A-Za-z]+'/ showLinenumbers title="TabsCustomizationExample.astro" --- - import { Tabs, TabItem, Card } from '@studiocms/ui/components'; + import { Tabs, TabItem, Card } from 'studiocms:ui/components'; --- @@ -148,7 +148,7 @@ this page. ```astro "variant='starlight'" showLinenumbers title="TabsVariantsExample.astro" --- - import { Tabs, TabItem, Card } from '@studiocms/ui/components'; + import { Tabs, TabItem, Card } from 'studiocms:ui/components'; --- @@ -197,7 +197,7 @@ In certain cases, you might want to change the alignment of the tabs. You can do ```astro "align='right'" showLinenumbers title="TabsAlignmentExample.astro" --- - import { Tabs, TabItem, Card } from '@studiocms/ui/components'; + import { Tabs, TabItem, Card } from 'studiocms:ui/components'; --- @@ -257,7 +257,7 @@ set the `storage` prop to `persistent` to store the value in localStorage. ```astro "syncKey='sync-example'" showLinenumbers title="TabsSyncExample.astro" --- - import { Tabs, TabItem, Card, Divider } from '@studiocms/ui/components'; + import { Tabs, TabItem, Card, Divider } from 'studiocms:ui/components'; --- diff --git a/docs/src/content/docs/docs/components/textarea.mdx b/docs/src/content/docs/docs/components/textarea.mdx index 4eff845..cb83b52 100644 --- a/docs/src/content/docs/docs/components/textarea.mdx +++ b/docs/src/content/docs/docs/components/textarea.mdx @@ -5,7 +5,7 @@ title: Textarea import { Tabs, TabItem } from '@astrojs/starlight/components'; import PreviewCard from '~/components/PreviewCard.astro'; -import { Textarea } from '@studiocms/ui/components'; +import { Textarea } from 'studiocms:ui/components'; A simple textarea component with support for easy labels and placeholders. @@ -20,7 +20,7 @@ A simple textarea component with support for easy labels and placeholders. ```astro showLinenumbers title="TextareaExample.astro" --- - import { Textarea } from '@studiocms/ui/components/Button'; + import { Textarea } from 'studiocms:ui/components'; --- - diff --git a/packages/studiocms_ui/src/components/Textarea/textarea.css b/packages/studiocms_ui/src/components/Textarea/textarea.css new file mode 100644 index 0000000..a18904a --- /dev/null +++ b/packages/studiocms_ui/src/components/Textarea/textarea.css @@ -0,0 +1,58 @@ +.sui-textarea-label { + display: flex; + flex-direction: column; + gap: .25rem; + max-width: 80ch; +} + +.sui-textarea-label.disabled { + opacity: 0.5; + pointer-events: none; + color: hsl(var(--text-muted)); +} + +.sui-textarea-label.full-width { + width: 100%; + max-width: none; +} + +.sui-textarea-label.full-height { + height: 100%; +} + +.label { + font-size: 14px; +} + +.sui-textarea { + padding: .65rem; + border-radius: var(--radius-md); + border: 1px solid hsl(var(--border)); + background: hsl(var(--background-step-2)); + color: hsl(var(--text-normal)); + transition: all .15s ease; + resize: none; + min-height: 12ch; + width: 100%; + height: 100%; +} + +.sui-textarea:hover { + background: hsl(var(--background-step-3)); +} + +.resize .sui-textarea { + resize: both; +} + +.sui-textarea:active, +.sui-textarea:focus { + border: 1px solid hsl(var(--primary-base)); + outline: none; + background: hsl(var(--background-step-2)); +} + +.req-star { + color: hsl(var(--danger-base)); + font-weight: 700; +} diff --git a/packages/studiocms_ui/src/components/ThemeToggle.astro b/packages/studiocms_ui/src/components/ThemeToggle.astro deleted file mode 100644 index 3573553..0000000 --- a/packages/studiocms_ui/src/components/ThemeToggle.astro +++ /dev/null @@ -1,46 +0,0 @@ ---- -import Button, { type Props as ButtonProps } from './Button.astro'; - -interface Props extends ButtonProps {} - -const props = Astro.props; ---- - - - - - - diff --git a/packages/studiocms_ui/src/components/ThemeToggle/ThemeToggle.astro b/packages/studiocms_ui/src/components/ThemeToggle/ThemeToggle.astro new file mode 100644 index 0000000..9cb7b13 --- /dev/null +++ b/packages/studiocms_ui/src/components/ThemeToggle/ThemeToggle.astro @@ -0,0 +1,21 @@ +--- +import Button, { type Props as ButtonProps } from '../Button/Button.astro'; +import './themetoggle.css'; + +type Props = ButtonProps; + +const props = Astro.props; +--- + + + + diff --git a/packages/studiocms_ui/src/components/ThemeToggle/themetoggle.css b/packages/studiocms_ui/src/components/ThemeToggle/themetoggle.css new file mode 100644 index 0000000..a10666f --- /dev/null +++ b/packages/studiocms_ui/src/components/ThemeToggle/themetoggle.css @@ -0,0 +1,18 @@ +#sui-theme-toggle, #sui-theme-toggle * { + color: hsl(var(--text-normal)); +} + +#sui-theme-toggle #dark-content, #sui-theme-toggle #light-content { + display: none; + width: fit-content; + height: fit-content; + max-height: 100%; +} + +[data-theme="dark"] #sui-theme-toggle #dark-content { + display: block; +} + +[data-theme="light"] #sui-theme-toggle #light-content { + display: block; +} diff --git a/packages/studiocms_ui/src/components/ThemeToggle/themetoggle.ts b/packages/studiocms_ui/src/components/ThemeToggle/themetoggle.ts new file mode 100644 index 0000000..9ccd01f --- /dev/null +++ b/packages/studiocms_ui/src/components/ThemeToggle/themetoggle.ts @@ -0,0 +1,6 @@ +import { ThemeHelper } from '../../utils/ThemeHelper.js'; + +const themeToggle = document.getElementById('sui-theme-toggle'); +const themeHelper = new ThemeHelper(); + +themeHelper.registerToggle(themeToggle); diff --git a/packages/studiocms_ui/src/components/Toast/Toaster.astro b/packages/studiocms_ui/src/components/Toast/Toaster.astro index f31da16..712cb83 100644 --- a/packages/studiocms_ui/src/components/Toast/Toaster.astro +++ b/packages/studiocms_ui/src/components/Toast/Toaster.astro @@ -1,4 +1,6 @@ --- +import './toaster.css'; + /** * Props for the Toast component. */ @@ -63,408 +65,5 @@ const { /> - - diff --git a/packages/studiocms_ui/src/components/Toast/index.ts b/packages/studiocms_ui/src/components/Toast/index.ts deleted file mode 100644 index 6a45221..0000000 --- a/packages/studiocms_ui/src/components/Toast/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as Toaster } from './Toaster.astro'; -export { toast } from './toast'; diff --git a/packages/studiocms_ui/src/components/Toast/toast.ts b/packages/studiocms_ui/src/components/Toast/toast.ts index b3974e4..063fdfd 100644 --- a/packages/studiocms_ui/src/components/Toast/toast.ts +++ b/packages/studiocms_ui/src/components/Toast/toast.ts @@ -1,4 +1,4 @@ -import type { ToastProps } from '../../types'; +import type { ToastProps } from '../../types/index.js'; /** * A function to create toasts with. diff --git a/packages/studiocms_ui/src/components/Toast/toaster.css b/packages/studiocms_ui/src/components/Toast/toaster.css new file mode 100644 index 0000000..f20755d --- /dev/null +++ b/packages/studiocms_ui/src/components/Toast/toaster.css @@ -0,0 +1,195 @@ +#sui-toaster { + width: 100vw; + height: 100vh; + position: fixed; + top: 0; + left: 0; + z-index: 100; + pointer-events: none; + color: hsl(var(--text-normal)); +} + +#sui-toast-drawer { + max-width: 420px; + width: 100%; + height: fit-content; + position: absolute; + display: flex; + flex-direction: column; +} + +#sui-toaster.top-left #sui-toast-drawer, +#sui-toaster.bottom-left #sui-toast-drawer { + left: 50%; + transform: translateX(-50%); +} + +.sui-toast-container { + pointer-events: all; + padding: 1rem; + border-radius: var(--radius-md); + border: 1px solid hsl(var(--border)); + background-color: hsl(var(--background-base)); + box-shadow: 0px 4px 8px hsl(var(--shadow), 0.5); + display: flex; + flex-direction: column; + gap: .5rem; + position: relative; + overflow: hidden; + margin-bottom: var(--gap); + animation: toast-pop-in .25s ease forwards; + z-index: 90; +} + +.sui-toast-header { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; +} + +.sui-toast-header-left-side { + display: flex; + flex-direction: row; + gap: .5rem; + align-items: center; + font-weight: 500; + font-size: 1.125em; +} + +.sui-toast-header-left-side svg { + color: hsl(var(--primary-base)); +} + +.sui-toast-container.success .sui-toast-header-left-side svg { + color: hsl(var(--success-base)); +} + +.sui-toast-container.warning .sui-toast-header-left-side svg { + color: hsl(var(--warning-base)); +} + +.sui-toast-container.danger .sui-toast-header-left-side svg { + color: hsl(var(--danger-base)); +} + +.sui-toast-container.info .sui-toast-header-left-side svg { + color: hsl(var(--info-base)); +} + +.sui-toast-container.mono .sui-toast-header-left-side svg { + color: hsl(var(--mono-base)); +} + +.sui-toast-progress-bar { + position: absolute; + height: 4px; + width: 100%; + bottom: 0; + left: 0%; + background-color: hsl(var(--primary-base)); + animation: toast-progress forwards linear; +} + +.sui-toast-container.paused .sui-toast-progress-bar { + animation-play-state: paused; +} + +.sui-toast-container.success .sui-toast-progress-bar { + background-color: hsl(var(--success-base)); +} + +.sui-toast-container.warning .sui-toast-progress-bar { + background-color: hsl(var(--warning-base)); +} + +.sui-toast-container.danger .sui-toast-progress-bar { + background-color: hsl(var(--danger-base)); +} + +.close-icon-container { + cursor: pointer; + height: 1.5rem; + width: 1.5rem; + display: flex; + align-items: center; + justify-content: center; + transition: background-color .15s ease; + border-radius: var(--radius-sm); +} + +.close-icon-container:hover { + background-color: hsl(var(--default-base)); +} + +.close-icon-container:focus-visible { + outline: 2px solid hsl(var(--text-normal)); + outline-offset: 2px; +} + +.sui-toast-container.closing { + animation: toast-closing .25s ease forwards; +} + +.sui-toast-container.persistent { + border: 1px solid hsl(var(--primary-base)); +} + +.sui-toast-container.persistent.success { + border: 1px solid hsl(var(--success-base)); +} + +.sui-toast-container.persistent.warning { + border: 1px solid hsl(var(--warning-base)); +} + +.sui-toast-container.persistent.danger { + border: 1px solid hsl(var(--danger-base)); +} + +@keyframes toast-pop-in { + 0% { + opacity: 0; + scale: 0.75; + } + 100% { + opacity: 1; + scale: 1; + } +} + +@keyframes toast-closing { + 0% { + opacity: 1; + scale: 1; + max-height: 500px; + margin-bottom: var(--gap); + padding: 1rem; + border: 1px solid hsl(var(--border)); + } + 62.5% { + scale: 0.75; + opacity: 0; + max-height: 500px; + margin-bottom: var(--gap); + padding: 1rem; + border: 1px solid hsl(var(--border)); + } + 100% { + scale: 0.75; + opacity: 0; + max-height: 0px; + margin-bottom: 0; + padding: 0; + border: 0px solid hsl(var(--border)); + } +} + +@keyframes toast-progress { + 0% { + left: 0%; + } + 100% { + left: -100%; + } +} diff --git a/packages/studiocms_ui/src/components/Toast/toaster.ts b/packages/studiocms_ui/src/components/Toast/toaster.ts new file mode 100644 index 0000000..8a3a0ee --- /dev/null +++ b/packages/studiocms_ui/src/components/Toast/toaster.ts @@ -0,0 +1,219 @@ +import type { ToastProps } from '../../types/index.js'; +import { generateID } from '../../utils/generateID.js'; +import { type ValidIconString, getIconString } from '../../utils/iconStrings.js'; + +let activeToasts: string[] = []; + +let lastActiveElement: HTMLElement | null = null; + +const revertFocusBackToLastActiveElement = () => { + if (lastActiveElement) { + lastActiveElement.focus(); + lastActiveElement = null; + } +}; + +/** + * Callback wrapper that allows for pausing, continuing and clearing a timer. Based on https://stackoverflow.com/a/20745721. + * @param callback The callback to be called. + * @param delay The delay in milliseconds. + */ +class Timer { + private id: NodeJS.Timeout | null; + private started: Date | null; + private remaining: number; + private running: boolean; + private callback: () => unknown; + + constructor(callback: () => unknown, delay: number) { + this.id = null; + this.started = null; + this.remaining = delay; + this.running = false; + this.callback = callback; + + this.start(); + } + + start = () => { + this.running = true; + this.started = new Date(); + this.id = setTimeout(this.callback, this.remaining); + }; + + pause = () => { + if (!this.id || !this.started || !this.running) return; + + this.running = false; + clearTimeout(this.id); + this.remaining -= new Date().getTime() - this.started.getTime(); + }; + + getTimeLeft = () => { + if (this.running) { + this.pause(); + this.start(); + } + + return this.remaining; + }; + + getStateRunning = () => { + return this.running; + }; +} + +function removeToast(toastID: string) { + const toastEl = document.getElementById(toastID); + + if (!toastEl) return; + + activeToasts = activeToasts.filter((x) => x !== toastID); + + toastEl.classList.add('closing'); + + setTimeout(() => toastEl.remove(), 400); +} + +function createToast(props: ToastProps) { + const toastParent = document.getElementById('sui-toast-drawer')! as HTMLDivElement; + + const toastContainer = document.createElement('div'); + const toastID = generateID('toast'); + toastContainer.tabIndex = 0; + toastContainer.ariaLive = 'polite'; + toastContainer.role = 'alert'; + toastContainer.id = toastID; + toastContainer.ariaLabel = `${props.title} (F8)`; + toastContainer.classList.add( + 'sui-toast-container', + `${props.closeButton || (props.persistent && 'closeable')}`, + `${props.persistent && 'persistent'}` + ); + + const toastHeader = document.createElement('div'); + toastHeader.classList.add('sui-toast-header'); + + const toastHeaderLeftSide = document.createElement('div'); + toastHeaderLeftSide.classList.add('sui-toast-header-left-side'); + + const toastTitle = document.createElement('span'); + toastTitle.textContent = props.title; + toastTitle.classList.add('sui-toast-title'); + + let iconString: ValidIconString; + + if (props.type === 'success') { + iconString = 'check-circle'; + } else if (props.type === 'danger') { + iconString = 'exclamation-circle'; + } else if (props.type === 'warning') { + iconString = 'exclamation-triangle'; + } else { + iconString = 'information-circle'; + } + + const toastIcon = getIconString(iconString, 'toast-icon', 24, 24); + toastHeaderLeftSide.innerHTML = toastIcon; + + toastHeaderLeftSide.appendChild(toastTitle); + toastHeader.appendChild(toastHeaderLeftSide); + + if (props.closeButton || props.persistent) { + const closeIconContainer = document.createElement('button'); + closeIconContainer.classList.add('close-icon-container'); + closeIconContainer.addEventListener('click', () => removeToast(toastID)); + closeIconContainer.innerHTML = getIconString('x-mark', 'close-icon', 24, 24); + closeIconContainer.tabIndex = 0; + closeIconContainer.ariaLabel = 'Close toast'; + + toastHeader.appendChild(closeIconContainer); + } + + toastContainer.appendChild(toastHeader); + + if (props.description) { + const toastDesc = document.createElement('span'); + toastDesc.innerHTML = props.description; + toastDesc.classList.add('sui-toast-desc'); + + toastContainer.appendChild(toastDesc); + } + + if (!props.persistent) { + const toastProgressBar = document.createElement('div'); + toastProgressBar.classList.add('sui-toast-progress-bar'); + toastProgressBar.style.animationDuration = props.duration + ? `${props.duration}ms` + : `${toastParent.dataset.duration || 4000}ms`; + + toastContainer.appendChild(toastProgressBar); + } + + toastParent.appendChild(toastContainer); + + activeToasts.push(toastID); + + if (!props.persistent) { + const timer = new Timer( + () => removeToast(toastID), + props.duration || + (toastParent.dataset.duration ? Number.parseInt(toastParent.dataset.duration) : 4000) + ); + + const timerPauseWrapper = () => { + toastContainer.classList.add('paused'); + timer.pause(); + }; + + const timerStartWrapper = () => { + toastContainer.classList.remove('paused'); + timer.start(); + }; + + toastContainer.addEventListener('mouseenter', timerPauseWrapper); + toastContainer.addEventListener('focusin', timerPauseWrapper); + + toastContainer.addEventListener('mouseleave', timerStartWrapper); + toastContainer.addEventListener('focusout', () => { + const focusedOrHasFocused = toastContainer.matches(':focus-within'); + + if (!focusedOrHasFocused) { + revertFocusBackToLastActiveElement(); + } + + timerStartWrapper(); + }); + } + + toastContainer.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + e.preventDefault(); + removeToast(toastID); + revertFocusBackToLastActiveElement(); + } + }); +} + +document.addEventListener('createtoast', (e) => { + e.stopImmediatePropagation(); + + const event = e as CustomEvent; + + createToast(event.detail); +}); + +window.addEventListener('keydown', (e) => { + if (e.key === 'F8') { + e.preventDefault(); + + const oldestToast = activeToasts[0]; + + if (oldestToast) { + lastActiveElement = document.activeElement as HTMLElement; + + const toastEl = document.getElementById(oldestToast); + if (toastEl) toastEl?.focus(); + } + } +}); diff --git a/packages/studiocms_ui/src/components/Toggle.astro b/packages/studiocms_ui/src/components/Toggle.astro deleted file mode 100644 index 081b12c..0000000 --- a/packages/studiocms_ui/src/components/Toggle.astro +++ /dev/null @@ -1,214 +0,0 @@ ---- -import type { StudioCMSColorway } from '../utils/colors'; -import { generateID } from '../utils/generateID'; - -/** - * The props for the toggle component - */ -interface Props { - /** - * The label of the toggle. - */ - label: string; - /** - * The size of the toggle. Defaults to `md`. - */ - size?: 'sm' | 'md' | 'lg'; - /** - * The color of the toggle. Defaults to `default`. - */ - color?: StudioCMSColorway; - /** - * Whether the toggle is checked by default. Defaults to `false`. - */ - defaultChecked?: boolean; - /** - * Whether the toggle is disabled. Defaults to `false`. - */ - disabled?: boolean; - /** - * The name of the toggle. - */ - name?: string; - /** - * Whether the toggle is required. Defaults to `false`. - */ - isRequired?: boolean; -} - -const { - size = 'md', - color = 'default', - defaultChecked, - disabled, - name = generateID('checkbox'), - label, - isRequired, -} = Astro.props; ---- -
- - - - \ No newline at end of file diff --git a/playground/src/components/ToastTests.astro b/playground/src/components/ToastTests.astro deleted file mode 100644 index 981de80..0000000 --- a/playground/src/components/ToastTests.astro +++ /dev/null @@ -1,66 +0,0 @@ ---- -import { Button, Row, Toaster } from '@studiocms/ui/components'; ---- - - - - - - - - - - \ No newline at end of file diff --git a/playground/src/components/ToggleTests.astro b/playground/src/components/ToggleTests.astro deleted file mode 100644 index b059a95..0000000 --- a/playground/src/components/ToggleTests.astro +++ /dev/null @@ -1,77 +0,0 @@ ---- -import { Row, Toggle } from '@studiocms/ui/components'; ---- -

Sizes

- - - - - -

Colors

- - - - - - - -

Disabled

- - - - - - - - \ No newline at end of file diff --git a/playground/src/icons/ArrowLeft.astro b/playground/src/icons/ArrowLeft.astro deleted file mode 100644 index c9e24a7..0000000 --- a/playground/src/icons/ArrowLeft.astro +++ /dev/null @@ -1,13 +0,0 @@ ---- -import type { HTMLAttributes } from 'astro/types'; - -interface Props extends HTMLAttributes<'svg'> { - height?: number; - width?: number; -} - -const { height = 24, width = 24, ...props } = Astro.props; ---- - - - diff --git a/playground/src/icons/Hamburger.astro b/playground/src/icons/Hamburger.astro deleted file mode 100644 index d27b9e7..0000000 --- a/playground/src/icons/Hamburger.astro +++ /dev/null @@ -1,13 +0,0 @@ ---- -import type { HTMLAttributes } from 'astro/types'; - -interface Props extends HTMLAttributes<'svg'> { - height?: number; - width?: number; -} - -const { height = 24, width = 24, ...props } = Astro.props; ---- - - - diff --git a/playground/src/icons/Moon.astro b/playground/src/icons/Moon.astro deleted file mode 100644 index a937ef7..0000000 --- a/playground/src/icons/Moon.astro +++ /dev/null @@ -1,13 +0,0 @@ ---- -import type { HTMLAttributes } from 'astro/types'; - -interface Props extends HTMLAttributes<'svg'> { - height?: number; - width?: number; -} - -const { height = 24, width = 24, ...props } = Astro.props; ---- - - - \ No newline at end of file diff --git a/playground/src/icons/Sun.astro b/playground/src/icons/Sun.astro deleted file mode 100644 index bb17d6b..0000000 --- a/playground/src/icons/Sun.astro +++ /dev/null @@ -1,13 +0,0 @@ ---- -import type { HTMLAttributes } from 'astro/types'; - -interface Props extends HTMLAttributes<'svg'> { - height?: number; - width?: number; -} - -const { height = 24, width = 24, ...props } = Astro.props; ---- - - - \ No newline at end of file diff --git a/playground/src/ideas/folder-system-client-side.ts b/playground/src/ideas/folder-system-client-side.ts deleted file mode 100644 index 120f9c3..0000000 --- a/playground/src/ideas/folder-system-client-side.ts +++ /dev/null @@ -1,24 +0,0 @@ -const folders: string[] = []; - -const array: string[] = [ - '2024/24/slug', - '2024/26/slug', - '2023/11/slug', - '2024/12/slug', - '2022/13/slug', - '2023/14/slug', -]; - -for (let i = 0; i < array.length; i++) { - const item = array[i]; - - if (!item) continue; - - const firstItemFolder = item.split('/')[0]; - - if (firstItemFolder && !folders.find((x) => x === firstItemFolder)) { - folders.push(firstItemFolder); - } -} - -console.log(folders); diff --git a/playground/src/layouts/DoubleSidebarLayout.astro b/playground/src/layouts/DoubleSidebarLayout.astro deleted file mode 100644 index 5331e2e..0000000 --- a/playground/src/layouts/DoubleSidebarLayout.astro +++ /dev/null @@ -1,90 +0,0 @@ ---- -import 'fontsource-variable/onest/index.css'; -import Icon from '@/utils/Icon.astro'; -import { Button, DoubleSidebar } from '@studiocms/ui/components'; -import { RootLayout, type RootLayoutProps } from '@studiocms/ui/layouts'; - -type Props = RootLayoutProps; - -const { title, description, image, headers, lang = 'en', defaultTheme = 'dark' } = Astro.props; ---- - -
- -
- Components - Single Sidebar - -
-
- - -
- -
- -
-
- - diff --git a/playground/src/layouts/SidebarLayout.astro b/playground/src/layouts/SidebarLayout.astro deleted file mode 100644 index b51bdca..0000000 --- a/playground/src/layouts/SidebarLayout.astro +++ /dev/null @@ -1,45 +0,0 @@ ---- -import 'fontsource-variable/onest/index.css'; -import { Sidebar } from '@studiocms/ui/components'; -import { RootLayout, type RootLayoutProps } from '@studiocms/ui/layouts'; - -type Props = RootLayoutProps; - -const { title, description, image, headers, lang = 'en', defaultTheme = 'dark' } = Astro.props; ---- - -
- - Components - Double Sidebar - -
- -
-
-
- diff --git a/playground/src/pages/double-sidebar.astro b/playground/src/pages/double-sidebar.astro deleted file mode 100644 index f4bf7b6..0000000 --- a/playground/src/pages/double-sidebar.astro +++ /dev/null @@ -1,39 +0,0 @@ ---- -import DoubleSidebarLayout from '@/layouts/DoubleSidebarLayout.astro'; -import Icon from '@/utils/Icon.astro'; -import { Button } from '@studiocms/ui/components'; ---- - - - - Site Content - - - \ No newline at end of file diff --git a/playground/src/pages/index.astro b/playground/src/pages/index.astro deleted file mode 100644 index 7fe2878..0000000 --- a/playground/src/pages/index.astro +++ /dev/null @@ -1,183 +0,0 @@ ---- -import '@fontsource-variable/onest/index.css'; - -import { Button, Center, Divider, Input, Row, Textarea, User } from '@studiocms/ui/components'; - -import CardTests from '@/components/CardTests.astro'; -import CheckboxTests from '@/components/CheckboxTests.astro'; -import DropdownTests from '@/components/DropdownTests.astro'; -import ModalTests from '@/components/ModalTests.astro'; -import RadioGroupTests from '@/components/RadioGroupTests.astro'; -import SearchSelectTests from '@/components/SearchSelectTests.astro'; -import SelectTests from '@/components/SelectTests.astro'; -import ToastTests from '@/components/ToastTests.astro'; -import ToggleTests from '@/components/ToggleTests.astro'; -import Icon from '@/utils/Icon.astro'; -import ButtonTests from '../components/ButtonTests.astro'; ---- - - - - - - - Component Tests - - - -

Button

- -

Divider

- - Divider Content - -

Input

- - - -

Row

- - - - -

Center

-
- Centered Stuff -
-

Textarea

-