├── static
├── .nojekyll
├── cocktail.jpg
├── cocktail.mp4
├── favicon.png
├── cocktail-close-up.jpg
└── cocktail-close-up-2.jpg
├── src
├── lib
│ ├── stores
│ │ └── index.ts
│ ├── components
│ │ ├── input
│ │ │ ├── index.scss
│ │ │ ├── docs.ts
│ │ │ └── props.ts
│ │ ├── radio
│ │ │ ├── index.scss
│ │ │ ├── docs.ts
│ │ │ ├── QRadio.svelte
│ │ │ └── props.ts
│ │ ├── checkbox
│ │ │ ├── index.scss
│ │ │ ├── props.ts
│ │ │ ├── QCheckbox.svelte
│ │ │ └── docs.ts
│ │ ├── card
│ │ │ ├── QCardActions.scss
│ │ │ ├── QCardSection.scss
│ │ │ ├── QCardSection.svelte
│ │ │ ├── QCardActions.svelte
│ │ │ ├── QCard.scss
│ │ │ ├── QCard.svelte
│ │ │ ├── props.ts
│ │ │ └── docs.ts
│ │ ├── breadcrumbs
│ │ │ ├── QBreadcrumbs.scss
│ │ │ ├── docs.ts
│ │ │ ├── QBreadcrumbsEl.scss
│ │ │ ├── QBreadcrumbs.svelte
│ │ │ ├── props.ts
│ │ │ └── QBreadcrumbsEl.svelte
│ │ ├── private
│ │ │ ├── index.ts
│ │ │ ├── ContextReseter.svelte
│ │ │ ├── QIconSnippet.svelte
│ │ │ └── QDocsSection.svelte
│ │ ├── table
│ │ │ ├── docs.ts
│ │ │ ├── index.scss
│ │ │ └── props.ts
│ │ ├── dialog
│ │ │ ├── docs.ts
│ │ │ └── props.ts
│ │ ├── drawer
│ │ │ ├── docs.ts
│ │ │ ├── QDrawer.scss
│ │ │ └── props.ts
│ │ ├── footer
│ │ │ ├── docs.ts
│ │ │ ├── QFooter.scss
│ │ │ └── props.ts
│ │ ├── icon
│ │ │ ├── docs.ts
│ │ │ ├── QIcon.svelte
│ │ │ ├── props.ts
│ │ │ └── QIcon.scss
│ │ ├── railbar
│ │ │ ├── docs.ts
│ │ │ ├── props.ts
│ │ │ ├── QRailbar.scss
│ │ │ └── QRailbar.svelte
│ │ ├── header
│ │ │ ├── props.ts
│ │ │ ├── docs.ts
│ │ │ └── QHeader.scss
│ │ ├── toolbar
│ │ │ ├── QToolbarTitle.svelte
│ │ │ ├── QToolbar.svelte
│ │ │ ├── QToolbar.scss
│ │ │ ├── docs.ts
│ │ │ └── props.ts
│ │ ├── tooltip
│ │ │ ├── docs.ts
│ │ │ ├── QTooltip.scss
│ │ │ └── props.ts
│ │ ├── list
│ │ │ ├── QList.scss
│ │ │ ├── QItemSection.scss
│ │ │ ├── docs.ts
│ │ │ ├── QItem.scss
│ │ │ ├── QList.svelte
│ │ │ └── QItemSection.svelte
│ │ ├── layout
│ │ │ ├── docs.ts
│ │ │ └── props.ts
│ │ ├── button
│ │ │ └── docs.ts
│ │ ├── switch
│ │ │ ├── docs.ts
│ │ │ └── props.ts
│ │ ├── chip
│ │ │ ├── docs.ts
│ │ │ └── props.ts
│ │ ├── expansion-item
│ │ │ ├── docs.ts
│ │ │ └── QExpansionItem.scss
│ │ ├── avatar
│ │ │ ├── docs.ts
│ │ │ ├── QAvatar.svelte
│ │ │ ├── index.scss
│ │ │ └── QAvatar.scss
│ │ ├── select
│ │ │ ├── docs.ts
│ │ │ └── index.scss
│ │ ├── tabs
│ │ │ ├── docs.ts
│ │ │ ├── QTabs.scss
│ │ │ ├── props.ts
│ │ │ └── QTab.scss
│ │ ├── codeBlock
│ │ │ └── props.ts
│ │ ├── progress
│ │ │ ├── docs.ts
│ │ │ ├── QCircularProgress.scss
│ │ │ ├── QLinearProgress.scss
│ │ │ └── QLinearProgress.svelte
│ │ ├── separator
│ │ │ ├── docs.ts
│ │ │ ├── QSeparator.scss
│ │ │ ├── QSeparator.svelte
│ │ │ └── props.ts
│ │ └── index.ts
│ ├── helpers
│ │ ├── pageTitle.ts
│ │ ├── index.ts
│ │ └── clickOutside.ts
│ ├── composables
│ │ ├── index.ts
│ │ ├── useSize.ts
│ │ ├── useAlign.ts
│ │ └── useRouterLink.ts
│ ├── css
│ │ ├── classes
│ │ │ ├── _overflow.scss
│ │ │ ├── _position.scss
│ │ │ ├── _select.scss
│ │ │ ├── _index.scss
│ │ │ ├── _spaces.scss
│ │ │ ├── _grid.scss
│ │ │ ├── _design.scss
│ │ │ └── _flex.scss
│ │ ├── mixins
│ │ │ ├── _toolbar.scss
│ │ │ ├── _index.scss
│ │ │ ├── _typography.scss
│ │ │ ├── _layout.scss
│ │ │ ├── _table.scss
│ │ │ ├── _spaces.scss
│ │ │ ├── _responsive.scss
│ │ │ ├── _image.scss
│ │ │ ├── _menu.scss
│ │ │ ├── _design.scss
│ │ │ └── _field.scss
│ │ ├── theme
│ │ │ ├── _css-variables.scss
│ │ │ ├── _index.scss
│ │ │ ├── _page.scss
│ │ │ ├── _reset.scss
│ │ │ ├── _elevate.scss
│ │ │ └── _typography.scss
│ │ ├── index.scss
│ │ ├── _disabled.scss
│ │ ├── fonts.scss
│ │ ├── _ripple.scss
│ │ ├── _components.scss
│ │ └── _variables.scss
│ ├── global.d.ts
│ ├── index.ts
│ ├── utils
│ │ ├── clipboard.ts
│ │ ├── index.ts
│ │ ├── number.ts
│ │ ├── router.ts
│ │ ├── types.ts
│ │ ├── props.ts
│ │ └── context.ts
│ └── classes
│ │ └── QTheme.svelte.ts
├── routes
│ ├── +layout.ts
│ ├── colors
│ │ └── +page.svelte
│ ├── privacy-policy
│ │ └── +page.svelte
│ ├── grid
│ │ └── +page.svelte
│ ├── +page.svelte
│ ├── utils
│ │ └── +page.svelte
│ └── components
│ │ └── layout
│ │ └── docs.snippets.ts
├── index.test.ts
├── material-dynamic-colors.shim.d.ts
├── dev
│ ├── versionPlugin.ts
│ ├── writeVersion.ts
│ ├── colorgenPlugin.ts
│ └── waitForSvelteKit.ts
├── app.html
└── app.d.ts
├── .gitattributes
├── .npmrc
├── plugins
├── index.ts
└── class-preprocessor
│ ├── source.ts
│ ├── types.ts
│ ├── markup.ts
│ └── index.ts
├── scripts
├── colorgen.ts
├── writeVersion.ts
├── docgenProps.ts
└── docgenSnippets.ts
├── docgen
├── helpers
│ ├── extractHash.ts
│ ├── pathExists.ts
│ ├── generateHash.ts
│ ├── getComponentDirs.ts
│ └── formatCodeAndAddHash.ts
├── snippets
│ ├── updateAllSnippets.ts
│ ├── getSnippetPagePaths.ts
│ ├── updateSnippetsForPage.ts
│ ├── parseSvelteFile.ts
│ └── getInfo.ts
├── props
│ ├── updateDocTypesFile.ts
│ ├── getInfo.ts
│ ├── worker.ts
│ ├── updateAllProps.ts
│ └── WorkerManager.ts
└── types
│ └── parseTypes.ts
├── .prettierrc
├── .prettierignore
├── .gitignore
├── vite.config.scss.ts
├── .github
├── pull_request_template.md
└── workflows
│ ├── code-checks.yml
│ ├── npmpublish.yml
│ └── pages.yml
├── tsconfig.json
├── LICENSE
├── svelte.config.js
└── vite.config.ts
/static/.nojekyll:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/lib/stores/index.ts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 |
--------------------------------------------------------------------------------
/src/routes/+layout.ts:
--------------------------------------------------------------------------------
1 | export const ssr = false;
2 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
2 | resolution-mode=highest
3 |
--------------------------------------------------------------------------------
/src/lib/components/input/index.scss:
--------------------------------------------------------------------------------
1 | @use "$css/shared/q-field.scss";
2 |
--------------------------------------------------------------------------------
/static/cocktail.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quaffui/quaff/HEAD/static/cocktail.jpg
--------------------------------------------------------------------------------
/static/cocktail.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quaffui/quaff/HEAD/static/cocktail.mp4
--------------------------------------------------------------------------------
/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quaffui/quaff/HEAD/static/favicon.png
--------------------------------------------------------------------------------
/static/cocktail-close-up.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quaffui/quaff/HEAD/static/cocktail-close-up.jpg
--------------------------------------------------------------------------------
/plugins/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./contextPreprocessor.js";
2 | export * from "./class-preprocessor/index.js";
3 |
--------------------------------------------------------------------------------
/scripts/colorgen.ts:
--------------------------------------------------------------------------------
1 | import { writeColorFile } from "../src/dev/colorgenPlugin.js";
2 |
3 | writeColorFile();
4 |
--------------------------------------------------------------------------------
/scripts/writeVersion.ts:
--------------------------------------------------------------------------------
1 | import writeVersion from "../src/dev/writeVersion.js";
2 |
3 | await writeVersion();
4 |
--------------------------------------------------------------------------------
/static/cocktail-close-up-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quaffui/quaff/HEAD/static/cocktail-close-up-2.jpg
--------------------------------------------------------------------------------
/scripts/docgenProps.ts:
--------------------------------------------------------------------------------
1 | import updateAllProps from "../docgen/props/updateAllProps.js";
2 |
3 | updateAllProps();
4 |
--------------------------------------------------------------------------------
/src/lib/helpers/pageTitle.ts:
--------------------------------------------------------------------------------
1 | export function pageTitle(title: string) {
2 | return `${title} • Quaff`;
3 | }
4 |
--------------------------------------------------------------------------------
/src/lib/components/radio/index.scss:
--------------------------------------------------------------------------------
1 | @use "$css/mixins";
2 |
3 | .q-radio {
4 | @include mixins.selection(radio);
5 | }
6 |
--------------------------------------------------------------------------------
/src/lib/composables/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./useAlign";
2 | export * from "./useRouterLink";
3 | export * from "./useSize";
4 |
--------------------------------------------------------------------------------
/scripts/docgenSnippets.ts:
--------------------------------------------------------------------------------
1 | import updateAllSnippets from "../docgen/snippets/updateAllSnippets.js";
2 |
3 | updateAllSnippets();
4 |
--------------------------------------------------------------------------------
/src/lib/css/classes/_overflow.scss:
--------------------------------------------------------------------------------
1 | .scroll {
2 | overflow: scroll;
3 | }
4 |
5 | .no-overflow {
6 | overflow: hidden;
7 | }
8 |
--------------------------------------------------------------------------------
/src/lib/helpers/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./clickOutside";
2 | export * from "./ripple";
3 | export { default as version } from "./version";
4 |
--------------------------------------------------------------------------------
/src/lib/css/classes/_position.scss:
--------------------------------------------------------------------------------
1 | .absolute-full {
2 | position: absolute;
3 | top: 0;
4 | right: 0;
5 | bottom: 0;
6 | left: 0;
7 | }
8 |
--------------------------------------------------------------------------------
/src/lib/css/classes/_select.scss:
--------------------------------------------------------------------------------
1 | .no-select {
2 | -webkit-user-select: none;
3 | -moz-user-select: none;
4 | -ms-user-select: none;
5 | user-select: none;
6 | }
7 |
--------------------------------------------------------------------------------
/src/lib/css/classes/_index.scss:
--------------------------------------------------------------------------------
1 | @forward "design";
2 | @forward "flex";
3 | @forward "grid";
4 | @forward "overflow";
5 | @forward "position";
6 | @forward "select";
7 | @forward "spaces";
8 |
--------------------------------------------------------------------------------
/src/lib/global.d.ts:
--------------------------------------------------------------------------------
1 | declare let __PLATFORM__:
2 | | "aix"
3 | | "darwin"
4 | | "freebsd"
5 | | "linux"
6 | | "openbsd"
7 | | "sunos"
8 | | "win32"
9 | | "android";
10 |
--------------------------------------------------------------------------------
/src/index.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from "vitest";
2 |
3 | describe("sum test", () => {
4 | it("adds 1 + 2 to equal 3", () => {
5 | expect(1 + 2).toBe(3);
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/src/lib/css/mixins/_toolbar.scss:
--------------------------------------------------------------------------------
1 | @mixin toolbarDisplay() {
2 | display: flex;
3 | align-items: center;
4 | justify-content: flex-start;
5 | white-space: nowrap;
6 | gap: 1rem;
7 | }
8 |
--------------------------------------------------------------------------------
/src/lib/css/theme/_css-variables.scss:
--------------------------------------------------------------------------------
1 | :root {
2 | --size: 16px;
3 | --font: Roboto, "Helvetica Neue", "Arial Nova", "Nimbus Sans", Arial, sans-serif;
4 | --font-icon: "Material Symbols Outlined";
5 | }
6 |
--------------------------------------------------------------------------------
/src/material-dynamic-colors.shim.d.ts:
--------------------------------------------------------------------------------
1 | import type { materialDynamicColors } from "material-dynamic-colors";
2 |
3 | declare module "material-dynamic-colors" {
4 | export default materialDynamicColors;
5 | }
6 |
--------------------------------------------------------------------------------
/src/lib/components/checkbox/index.scss:
--------------------------------------------------------------------------------
1 | @use "$css/mixins";
2 |
3 | .q-checkbox {
4 | @include mixins.selection(checkbox);
5 |
6 | > span::before {
7 | content: "check_box_outline_blank";
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/lib/components/card/QCardActions.scss:
--------------------------------------------------------------------------------
1 | @use "$css/mixins";
2 |
3 | .q-card__actions {
4 | display: flex;
5 | @include mixins.margin("t-sm");
6 |
7 | &--vertical {
8 | flex-direction: column;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/docgen/helpers/extractHash.ts:
--------------------------------------------------------------------------------
1 | export default function extractHash(docsPropsContent: string) {
2 | const hashPattern = /@quaffHash ([a-f0-9]+)/;
3 | const match = docsPropsContent.match(hashPattern);
4 |
5 | return match?.[1] || null;
6 | }
7 |
--------------------------------------------------------------------------------
/src/lib/components/card/QCardSection.scss:
--------------------------------------------------------------------------------
1 | @use "$css/mixins";
2 |
3 | .q-card__section {
4 | @include mixins.padding("a-sm");
5 |
6 | &--horizontal {
7 | border-radius: 0;
8 | @include mixins.margin("t-sm");
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/docgen/helpers/pathExists.ts:
--------------------------------------------------------------------------------
1 | import { access } from "fs/promises";
2 |
3 | export default async function pathExists(filePath: string) {
4 | try {
5 | await access(filePath);
6 | return true;
7 | } catch {
8 | return false;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": false,
3 | "singleQuote": false,
4 | "trailingComma": "es5",
5 | "endOfLine": "lf",
6 | "printWidth": 100,
7 | "plugins": ["prettier-plugin-svelte"],
8 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
9 | }
10 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /.svelte-kit
5 | /package
6 | .env
7 | .env.*
8 | !.env.example
9 |
10 | # Ignore files for PNPM, NPM and YARN
11 | pnpm-lock.yaml
12 | package-lock.json
13 | yarn.lock
14 |
15 | docs.snippets.ts
16 | /src/lib/utils/types.json
17 |
--------------------------------------------------------------------------------
/src/lib/css/mixins/_index.scss:
--------------------------------------------------------------------------------
1 | @forward "design";
2 | @forward "field";
3 | @forward "image";
4 | @forward "layout";
5 | @forward "menu";
6 | @forward "responsive";
7 | @forward "selection";
8 | @forward "spaces";
9 | @forward "table";
10 | @forward "toolbar";
11 | @forward "typography";
12 |
--------------------------------------------------------------------------------
/src/lib/components/breadcrumbs/QBreadcrumbs.scss:
--------------------------------------------------------------------------------
1 | .q-breadcrumbs {
2 | width: fit-content;
3 |
4 | &__list {
5 | display: flex;
6 | align-items: center;
7 | justify-content: center;
8 |
9 | margin: 0;
10 | padding: 0;
11 |
12 | list-style: none;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/docgen/helpers/generateHash.ts:
--------------------------------------------------------------------------------
1 | import { createHash } from "crypto";
2 |
3 | export default function generateHash(fileContents: string) {
4 | const hash = createHash("sha256");
5 | const fullResult = hash.update(fileContents).digest("hex");
6 | return fullResult.slice(0, fullResult.length / 2);
7 | }
8 |
--------------------------------------------------------------------------------
/src/lib/index.ts:
--------------------------------------------------------------------------------
1 | import Quaff from "./classes/Quaff.svelte.js";
2 | import QTheme from "./classes/QTheme.svelte.js";
3 | import QScrollObserver from "./classes/QScrollObserver.svelte";
4 |
5 | // Reexport your entry components here
6 | export * from "$components";
7 |
8 | export { Quaff, QTheme, QScrollObserver };
9 |
--------------------------------------------------------------------------------
/src/lib/css/theme/_index.scss:
--------------------------------------------------------------------------------
1 | @forward "colors";
2 | @forward "color-classes";
3 | @forward "palette";
4 |
5 | @forward "reset";
6 | @forward "css-variables";
7 | @forward "page";
8 | @forward "elevate";
9 |
10 | @forward "typography";
11 | @forward "typography-classes";
12 | @forward "typography-variables";
13 |
--------------------------------------------------------------------------------
/docgen/helpers/getComponentDirs.ts:
--------------------------------------------------------------------------------
1 | import { readdir } from "fs/promises";
2 |
3 | export default async function getComponentDirs(componentsDir: string) {
4 | const dirents = await readdir(componentsDir, { withFileTypes: true });
5 | return dirents.filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name);
6 | }
7 |
--------------------------------------------------------------------------------
/src/dev/versionPlugin.ts:
--------------------------------------------------------------------------------
1 | import writeVersion from "./writeVersion.js";
2 | import type { Plugin } from "vite";
3 |
4 | function versionPlugin(): Plugin {
5 | return {
6 | name: "version-plugin",
7 | async configResolved() {
8 | return await writeVersion();
9 | },
10 | };
11 | }
12 |
13 | export default versionPlugin;
14 |
--------------------------------------------------------------------------------
/src/lib/components/private/index.ts:
--------------------------------------------------------------------------------
1 | import ContextReseter from "./ContextReseter.svelte";
2 | import QApi from "./QApi.svelte";
3 | import QDocs from "./QDocs.svelte";
4 | import QDocsSection from "./QDocsSection.svelte";
5 | import QIconSnippet from "./QIconSnippet.svelte";
6 |
7 | export { ContextReseter, QApi, QDocs, QDocsSection, QIconSnippet };
8 |
--------------------------------------------------------------------------------
/src/lib/css/mixins/_typography.scss:
--------------------------------------------------------------------------------
1 | @mixin typography($name: null) {
2 | font-family: var(--typescale-#{$name}-font-family-name);
3 | font-weight: var(--typescale-#{$name}-font-weight);
4 | font-size: var(--typescale-#{$name}-font-size);
5 | line-height: var(--typescale-#{$name}-line-height);
6 | letter-spacing: var(--typescale-#{$name}-letter-spacing);
7 | }
8 |
--------------------------------------------------------------------------------
/src/lib/utils/clipboard.ts:
--------------------------------------------------------------------------------
1 | export async function copy(text: string) {
2 | if (navigator.clipboard.write) {
3 | const blob = new Blob([text], { type: "text/plain" });
4 | const item = new ClipboardItem({ "text/plain": blob });
5 | await navigator.clipboard.write([item]);
6 | } else {
7 | await navigator.clipboard.writeText(text);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/lib/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./clipboard";
2 | export * from "./colors";
3 | export { default as QColors } from "./colors";
4 | export * from "./context";
5 | export * from "./dom";
6 | export * from "./events";
7 | export * from "./number";
8 | export * from "./props";
9 | export * from "./router";
10 | export * from "./string";
11 | export * from "./types";
12 |
--------------------------------------------------------------------------------
/docgen/snippets/updateAllSnippets.ts:
--------------------------------------------------------------------------------
1 | import getSnippetPagePaths from "./getSnippetPagePaths.js";
2 | import updateSnippetsForPage from "./updateSnippetsForPage.js";
3 |
4 | export default async function updateAllSnippets() {
5 | const snippetPagePaths = await getSnippetPagePaths();
6 |
7 | for (const snippetPagePath of snippetPagePaths) {
8 | await updateSnippetsForPage(snippetPagePath);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/lib/utils/number.ts:
--------------------------------------------------------------------------------
1 | export function isNumeric(input: number | string): input is number | `${number}` {
2 | return typeof input === "number" || !isNaN(Number(input));
3 | }
4 |
5 | export function between(value: number, min: number, max: number) {
6 | if (max < min) {
7 | console.warn(`max (${max}) should not be smaller than min (${min}).`);
8 | }
9 | return max <= min ? min : Math.min(max, Math.max(min, value));
10 | }
11 |
--------------------------------------------------------------------------------
/src/lib/components/private/ContextReseter.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 | {@render children?.()}
17 |
--------------------------------------------------------------------------------
/src/lib/components/table/docs.ts:
--------------------------------------------------------------------------------
1 | import type { QComponentDocs } from "$utils";
2 | import { QTableDocsProps, QTableDocsSnippets } from "./docs.props";
3 |
4 | export const QTableDocs: QComponentDocs = {
5 | name: "QTable",
6 | description: "Tables allow for a clear presentation of data sets.",
7 | docs: {
8 | props: QTableDocsProps,
9 | snippets: QTableDocsSnippets,
10 | methods: [],
11 | events: [],
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/src/lib/components/dialog/docs.ts:
--------------------------------------------------------------------------------
1 | import type { QComponentDocs } from "$utils";
2 | import { QDialogDocsProps, QDialogDocsSnippets } from "./docs.props";
3 |
4 | export const QDialogDocs: QComponentDocs = {
5 | name: "QDialog",
6 | description: "Dialogs provide important prompts in a user flow.",
7 | docs: {
8 | props: QDialogDocsProps,
9 | snippets: QDialogDocsSnippets,
10 | methods: [],
11 | events: [],
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/src/lib/css/index.scss:
--------------------------------------------------------------------------------
1 | @use "sass:meta";
2 |
3 | @layer theme, ripple, components, classes, disabled;
4 |
5 | @layer Quaff {
6 | @layer theme {
7 | @include meta.load-css("theme");
8 | }
9 |
10 | @layer ripple {
11 | @include meta.load-css("ripple");
12 | }
13 |
14 | @layer components {
15 | @include meta.load-css("components");
16 | }
17 |
18 | @layer classes {
19 | @include meta.load-css("classes");
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/lib/components/drawer/docs.ts:
--------------------------------------------------------------------------------
1 | import type { QComponentDocs } from "$utils";
2 | import { QDrawerDocsProps, QDrawerDocsSnippets } from "./docs.props";
3 |
4 | export const QDrawerDocs: QComponentDocs = {
5 | name: "QDrawer",
6 | description: "Navigation drawers provide ergonomic access to destinations in an app",
7 | docs: {
8 | props: QDrawerDocsProps,
9 | snippets: QDrawerDocsSnippets,
10 | methods: [],
11 | events: [],
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /dist
5 | /.svelte-kit
6 | /package
7 | .env
8 | .env.*
9 | !.env.example
10 | vite.config.js.timestamp-*
11 | vite.config.ts.timestamp-*
12 | .vscode
13 | .vscode/*
14 |
15 | src/lib/helpers/version.ts
16 | src/lib/components/**/docs.props.ts
17 | src/routes/components/**/docs.snippets.ts
18 | !src/routes/components/layout/docs.snippets.ts
19 | src/lib/utils/types.json
20 | parseInterface.d.ts
21 | /plugins/dist/
22 |
--------------------------------------------------------------------------------
/src/lib/css/_disabled.scss:
--------------------------------------------------------------------------------
1 | @use "$css/mixins";
2 |
3 | @mixin base() {
4 | cursor: not-allowed;
5 | box-shadow: none;
6 | user-select: none;
7 | color: font-color();
8 | background-color: transparent;
9 | transition: none;
10 | }
11 |
12 | @function font-color($color: on-surface) {
13 | @return mixins.with-alpha(var(--#{$color}), 38%);
14 | }
15 |
16 | @function rest-color($color: on-surface) {
17 | @return mixins.with-alpha(var(--#{$color}), 12%);
18 | }
19 |
--------------------------------------------------------------------------------
/src/lib/components/footer/docs.ts:
--------------------------------------------------------------------------------
1 | import type { QComponentDocs } from "$utils";
2 | import { QFooterDocsProps, QFooterDocsSnippets } from "./docs.props";
3 |
4 | export const QFooterDocs: QComponentDocs = {
5 | name: "QFooter",
6 | description:
7 | "Footers can be used to display navigation and key actions at the bottom of the screen.",
8 | docs: {
9 | props: QFooterDocsProps,
10 | snippets: QFooterDocsSnippets,
11 | methods: [],
12 | events: [],
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/src/lib/components/icon/docs.ts:
--------------------------------------------------------------------------------
1 | import type { QComponentDocs } from "$utils";
2 | import { QIconDocsProps, QIconDocsSnippets } from "./docs.props";
3 |
4 | export const QIconDocs: QComponentDocs = {
5 | name: "QIcon",
6 | description:
7 | "This component allows you to insert icons within elements of the page. Supported cions are Material Symbols icons.",
8 | docs: {
9 | props: QIconDocsProps,
10 | snippets: QIconDocsSnippets,
11 | methods: [],
12 | events: [],
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/src/lib/components/railbar/docs.ts:
--------------------------------------------------------------------------------
1 | import type { QComponentDocs } from "$utils";
2 | import { QRailbarDocsProps, QRailbarDocsSnippets } from "./docs.props";
3 |
4 | export const QRailbarDocs: QComponentDocs = {
5 | name: "QRailbar",
6 | description:
7 | "Railbars are used to provide navigation between different sections or views within an application.",
8 | docs: {
9 | props: QRailbarDocsProps,
10 | snippets: QRailbarDocsSnippets,
11 | methods: [],
12 | events: [],
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/src/lib/components/header/props.ts:
--------------------------------------------------------------------------------
1 | import type { QToolbarProps } from "../toolbar/props";
2 |
3 | export interface QHeaderProps extends QToolbarProps {
4 | /**
5 | * @default false
6 | */
7 | elevated?: boolean;
8 | /**
9 | * @default false
10 | */
11 | bordered?: boolean;
12 | /**
13 | * @default 64
14 | */
15 | height?: number;
16 | /**
17 | * @default false
18 | */
19 | reveal?: boolean;
20 | /**
21 | * @default 250
22 | */
23 | revealOffset?: number;
24 | }
25 |
--------------------------------------------------------------------------------
/src/lib/css/theme/_page.scss:
--------------------------------------------------------------------------------
1 | @use "sass:map";
2 | @use "$css/variables";
3 |
4 | .q-page {
5 | max-width: 75rem;
6 | margin: 0 auto;
7 | padding: 4rem 1.5rem;
8 | line-height: 1.5;
9 |
10 | @media only screen and (max-width: #{map.get(variables.$breakpoints, md) - 1px}) {
11 | padding: 3rem 1.25rem;
12 | }
13 |
14 | @media only screen and (min-width: #{map.get(variables.$breakpoints, md)}) and (max-width: #{map.get(variables.$breakpoints, lg) - 1px}) {
15 | padding: 3.5rem 1.5rem;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/lib/components/toolbar/QToolbarTitle.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 | {@render children?.()}
18 |
19 |
--------------------------------------------------------------------------------
/src/lib/components/checkbox/props.ts:
--------------------------------------------------------------------------------
1 | import type { HTMLAttributes } from "svelte/elements";
2 |
3 | export interface QCheckboxProps extends HTMLAttributes {
4 | /**
5 | * Controls the checked state of the checkbox.
6 | */
7 | value: boolean;
8 | /**
9 | * Sets the label for the checkbox.
10 | * @default undefined
11 | */
12 | label?: string;
13 | /**
14 | * Puts the checkbox in a disabled state, making it unclickable.
15 | * @default false
16 | */
17 | disable?: boolean;
18 | }
19 |
--------------------------------------------------------------------------------
/src/lib/components/breadcrumbs/docs.ts:
--------------------------------------------------------------------------------
1 | import type { QComponentDocs } from "$utils";
2 | import { QBreadcrumbsDocsProps, QBreadcrumbsDocsSnippets } from "./docs.props";
3 |
4 | export const QBreadcrumbsDocs: QComponentDocs = {
5 | name: "QBreadcrumbs",
6 | description:
7 | "Breadcrumbs are mostly used as a navigation aid. They allow users to keep track of their location within the page.",
8 | docs: {
9 | props: QBreadcrumbsDocsProps,
10 | snippets: QBreadcrumbsDocsSnippets,
11 | methods: [],
12 | events: [],
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/src/lib/components/card/QCardSection.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 | {@render children?.()}
18 |
19 |
--------------------------------------------------------------------------------
/src/lib/components/tooltip/docs.ts:
--------------------------------------------------------------------------------
1 | import { QToolbarDocsSnippets } from "$components/header/docs.props";
2 | import type { QComponentDocs } from "$utils";
3 | import { QTooltipDocsProps } from "./docs.props";
4 |
5 | export const QTooltipDocs: QComponentDocs = {
6 | name: "QTooltip",
7 | description:
8 | "The Tooltip component displays informative text on hover or focus, providing additional context.",
9 | docs: {
10 | props: QTooltipDocsProps,
11 | snippets: QToolbarDocsSnippets,
12 | methods: [],
13 | events: [],
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/src/lib/components/list/QList.scss:
--------------------------------------------------------------------------------
1 | @use "$css/mixins";
2 |
3 | .q-list {
4 | display: flex;
5 | flex-direction: column;
6 | align-items: center;
7 | justify-content: center;
8 | width: 100%;
9 |
10 | &--bordered {
11 | @include mixins.border;
12 | }
13 |
14 | &--rounded {
15 | border-radius: 0.25rem;
16 | }
17 |
18 | &--dense {
19 | min-height: 2rem;
20 | padding-block: 0;
21 |
22 | & > .q-item {
23 | min-height: 2rem;
24 | }
25 | }
26 |
27 | & > .q-separator__wrapper:first-child {
28 | display: none !important;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/lib/css/fonts.scss:
--------------------------------------------------------------------------------
1 | @use "sass:string";
2 | @use "@fontsource/roboto";
3 |
4 | @mixin font-face-declaration($font-name) {
5 | $file-name: string.to-lower-case($font-name);
6 |
7 | @font-face {
8 | font-family: "Material Symbols #{$font-name}";
9 | src: url("material-symbols/material-symbols-#{$file-name}.woff2") format("woff2");
10 | font-style: normal;
11 | font-weight: 100 700;
12 | font-display: block;
13 | }
14 | }
15 |
16 | @include font-face-declaration("Outlined");
17 | @include font-face-declaration("Rounded");
18 | @include font-face-declaration("Sharp");
19 |
--------------------------------------------------------------------------------
/src/lib/css/classes/_spaces.scss:
--------------------------------------------------------------------------------
1 | @use "../mixins";
2 | @use "../variables";
3 |
4 | @each $spaceName, $spaceVal in variables.$spaces {
5 | @each $posName, $_ in variables.$positions {
6 | // Padding
7 | .q-p#{$posName}-#{$spaceName} {
8 | @include mixins.padding("#{$posName}-#{$spaceName}");
9 | }
10 |
11 | // Margin
12 | .q-m#{$posName}-#{$spaceName} {
13 | @include mixins.margin("#{$posName}-#{$spaceName}");
14 | }
15 | }
16 |
17 | // Gap
18 | @if $spaceName != "auto" {
19 | .q-gap-#{$spaceName} {
20 | gap: $spaceVal;
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/dev/writeVersion.ts:
--------------------------------------------------------------------------------
1 | import { readFile, writeFile } from "fs/promises";
2 |
3 | export default async function writeVersion() {
4 | const projectRootUrl = new URL("../../", import.meta.url);
5 | const packageJsonUrl = new URL("./package.json", projectRootUrl);
6 | const versionFileUrl = new URL("./src/lib/helpers/version.ts", projectRootUrl);
7 |
8 | const packageJsonData = await readFile(packageJsonUrl, "utf-8");
9 | const { version } = JSON.parse(packageJsonData);
10 | const content = `export default "${version}";\n`;
11 |
12 | return await writeFile(versionFileUrl, content);
13 | }
14 |
--------------------------------------------------------------------------------
/src/lib/components/layout/docs.ts:
--------------------------------------------------------------------------------
1 | import type { QComponentDocs } from "$utils";
2 | import { QLayoutDocsProps, QLayoutDocsSnippets } from "./docs.props";
3 |
4 | export const QLayoutDocs: QComponentDocs = {
5 | name: "QLayout",
6 | description:
7 | "The QLayout component is designed to be the skeleton of the entire page, with navigational elements such as header, railbars, drawers and footer. This component is not mandatory but it really helps structure the page.",
8 | docs: {
9 | props: QLayoutDocsProps,
10 | snippets: QLayoutDocsSnippets,
11 | methods: [],
12 | events: [],
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/src/lib/components/radio/docs.ts:
--------------------------------------------------------------------------------
1 | import type { QComponentDocs } from "$utils";
2 | import { QRadioDocsProps, QRadioDocsSnippets } from "./docs.props";
3 |
4 | export const QRadioDocs: QComponentDocs = {
5 | name: "QRadio",
6 | description: "Radio buttons allow the user to select one option from a set.",
7 | docs: {
8 | props: QRadioDocsProps,
9 | snippets: QRadioDocsSnippets,
10 | methods: [],
11 | events: [
12 | {
13 | name: "change",
14 | type: "(e: Event) => void",
15 | description: "Emitted when the radio button is selected.",
16 | },
17 | ],
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/plugins/class-preprocessor/source.ts:
--------------------------------------------------------------------------------
1 | import type MagicString from "magic-string";
2 | import type { Result } from "./types.js";
3 |
4 | export function changeSource(source: MagicString, result: Result) {
5 | // Remove the classes definition
6 | source.remove(result.def.start, result.def.end);
7 |
8 | if (!result.uses.length) {
9 | return;
10 | }
11 |
12 | for (const use of result.uses) {
13 | // As the class is of the form class="q-component",
14 | // We need to change it to class={["q-component",...]}
15 | source.update(use.start - 1, use.end + 1, `{[${result.def.classes.join(",")}]}`);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/lib/components/header/docs.ts:
--------------------------------------------------------------------------------
1 | import type { QComponentDocs } from "$utils";
2 | import { QHeaderDocsProps, QHeaderDocsSnippets } from "./docs.props";
3 |
4 | export const QHeaderDocs: QComponentDocs = {
5 | name: "QHeader",
6 | description:
7 | "QHeader is a component used for the top section of a QLayout, typically containing a QToolbarTitle for titles, navigation, and actions. It can be configured to be elevated, bordered, reveal on scroll, and have a custom height.",
8 | docs: {
9 | props: QHeaderDocsProps,
10 | snippets: QHeaderDocsSnippets,
11 | methods: [],
12 | events: [],
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/src/lib/css/mixins/_layout.scss:
--------------------------------------------------------------------------------
1 | @use "../variables";
2 |
3 | @mixin include-drawers($baseWidth, $pos, $el) {
4 | // Left drawer
5 | &.q-drawer--left ~ .q-#{$el} {
6 | width: calc(#{$baseWidth} - #{variables.$left-drawer});
7 | }
8 |
9 | // Right drawer
10 | &.q-drawer--right ~ .q-#{$el} {
11 | width: calc(#{$baseWidth} - #{variables.$right-drawer});
12 | }
13 |
14 | // Left drawer + Right drawer
15 | &.q-drawer--left
16 | ~ .q-drawer--active.q-drawer--right:not(.q-drawer--offset-#{$pos}, .q-drawer--overlay)
17 | ~ .q-#{$el} {
18 | width: calc(#{$baseWidth} - #{variables.$left-right-drawer});
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/lib/components/checkbox/QCheckbox.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 | {label}
19 |
20 |
--------------------------------------------------------------------------------
/src/lib/components/railbar/props.ts:
--------------------------------------------------------------------------------
1 | import type { NativeProps } from "$utils";
2 | import type { HTMLAttributes } from "svelte/elements";
3 |
4 | export interface QRailbarProps extends NativeProps, HTMLAttributes {
5 | /**
6 | * Width of the railbar in pixels.
7 | *
8 | * @default 88
9 | */
10 | width?: number;
11 |
12 | /**
13 | * Position of the railbar on the screen.
14 | *
15 | * @default left
16 | */
17 | side?: "left" | "right";
18 |
19 | /**
20 | * Adds a border to the railbar to separate it from the main content.
21 | *
22 | * @default false
23 | */
24 | bordered?: boolean;
25 | }
26 |
--------------------------------------------------------------------------------
/src/lib/components/checkbox/docs.ts:
--------------------------------------------------------------------------------
1 | import type { QComponentDocs } from "$utils";
2 | import { QCheckboxDocsProps, QCheckboxDocsSnippets } from "./docs.props";
3 |
4 | export const QCheckboxDocs: QComponentDocs = {
5 | name: "QCheckbox",
6 | description: "Checkboxes allow the user to select one or more items from a set.",
7 | docs: {
8 | props: QCheckboxDocsProps,
9 | snippets: QCheckboxDocsSnippets,
10 | methods: [],
11 | events: [
12 | {
13 | name: "change",
14 | type: "(e: Event) => void",
15 | description: "Emitted when the checkbox is checked or unchecked.",
16 | },
17 | ],
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/src/lib/components/button/docs.ts:
--------------------------------------------------------------------------------
1 | import type { QComponentDocs } from "$utils";
2 | import { QBtnDocsProps, QBtnDocsSnippets } from "./docs.props";
3 |
4 | export const QBtnDocs: QComponentDocs = {
5 | name: "QBtn",
6 | description:
7 | "Buttons help users take action, such as sending an email, sharing a document, or liking a comment.",
8 | docs: {
9 | props: QBtnDocsProps,
10 | snippets: QBtnDocsSnippets,
11 | methods: [],
12 | events: [
13 | {
14 | name: "click",
15 | type: "(e: MouseEvent) => void",
16 | description: "Emitted when the user clicks on the button.",
17 | },
18 | ],
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/vite.config.scss.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from "path";
2 |
3 | export default {
4 | css: {
5 | preprocessorOptions: {
6 | scss: {
7 | api: "modern-compiler",
8 | },
9 | },
10 | },
11 | resolve: {
12 | alias: {
13 | $lib: resolve(__dirname, "src/lib"),
14 | $components: resolve(__dirname, "./src/lib/components"),
15 | $css: resolve(__dirname, "./src/lib/css"),
16 | },
17 | },
18 | build: {
19 | emptyOutDir: false,
20 | rollupOptions: {
21 | input: "src/lib/css/index.scss",
22 | output: {
23 | dir: "dist/css",
24 | assetFileNames: `[name].[ext]`,
25 | },
26 | },
27 | },
28 | };
29 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## PR Type
2 |
3 | > What kind of change does this PR introduce?
4 |
5 | - [ ] Bugfix
6 | - [ ] Feature
7 | - [ ] Documentation
8 | - [ ] Code style update
9 | - [ ] Refactor
10 | - [ ] Build-related changes
11 | - [ ] Other, please describe:
12 |
13 | ## Does this PR introduce a breaking change?
14 |
15 | - [ ] Yes
16 | - [ ] No
17 |
18 | ## What's new?
19 |
20 | > List the changes
21 |
22 | -
23 |
24 | ## Screenshots
25 |
26 | > If needed, you can add screenshots here
27 |
28 | N/A
29 |
30 | ## This pull request closes an issue
31 |
32 | > Add `Closes`, `Fixes` or `Resolves` followed by `#xxx[,#xxx]` where "xxx" is the issue number
33 |
34 | N/A
35 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.svelte-kit/tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "checkJs": true,
6 | "lib": ["es2021", "es2022", "es2023", "dom"],
7 | "outDir": "./dist",
8 | "esModuleInterop": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "resolveJsonModule": true,
11 | "skipLibCheck": true,
12 | "sourceMap": true,
13 | "strict": true,
14 | "moduleResolution": "bundler",
15 | "preserveValueImports": false,
16 | "verbatimModuleSyntax": false
17 | },
18 | "exclude": ["./dist/**/*", "./plugins/dist/**/*"],
19 | "include": ["./docgen/**/*", "./scripts/**/*", "./src/**/*", "./plugins/**/*"]
20 | }
21 |
--------------------------------------------------------------------------------
/src/lib/components/radio/QRadio.svelte:
--------------------------------------------------------------------------------
1 |
21 |
22 |
23 |
24 | {label}
25 |
26 |
--------------------------------------------------------------------------------
/docgen/helpers/formatCodeAndAddHash.ts:
--------------------------------------------------------------------------------
1 | import { fileURLToPath } from "url";
2 | import prettier from "prettier";
3 |
4 | async function formatCode(code: string) {
5 | const prettierRcUrl = new URL("../../.prettierrc", import.meta.url);
6 | const options = await prettier.resolveConfig(fileURLToPath(prettierRcUrl));
7 |
8 | return prettier.format(code, { ...options, parser: "typescript" });
9 | }
10 |
11 | export default async function formatCodeAndAddHash(code: string, hash: string) {
12 | const formatted = await formatCode(code);
13 | return [
14 | "// AUTO GENERATED FILE - DO NOT MODIFY OR DELETE",
15 | `// @quaffHash ${hash}`,
16 | "",
17 | formatted,
18 | ].join("\n");
19 | }
20 |
--------------------------------------------------------------------------------
/src/lib/components/toolbar/QToolbar.svelte:
--------------------------------------------------------------------------------
1 |
24 |
25 |
26 | {@render children?.()}
27 |
28 |
--------------------------------------------------------------------------------
/.github/workflows/code-checks.yml:
--------------------------------------------------------------------------------
1 | name: Code Checks
2 |
3 | on:
4 | pull_request:
5 | types: [opened, synchronize]
6 |
7 | jobs:
8 | run-checks:
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - uses: actions/checkout@v5
13 |
14 | - uses: oven-sh/setup-bun@v2
15 | with:
16 | bun-version: 1.3.1
17 |
18 | - name: Prep
19 | run: bun install --frozen-lockfile && bun run write-version && bun run docgen-props && bun run docgen-snippets
20 |
21 | - name: Run plugins generation
22 | run: bun run gen:plugins
23 |
24 | - name: Run Type Check
25 | run: bun run check
26 |
27 | - name: Run Lint
28 | run: bun run lint
29 |
--------------------------------------------------------------------------------
/src/lib/components/switch/docs.ts:
--------------------------------------------------------------------------------
1 | import type { QComponentDocs } from "$utils";
2 | import { QSwitchDocsProps, QSwitchDocsSnippets } from "./docs.props";
3 |
4 | export const QSwitchDocs: QComponentDocs = {
5 | name: "QSwitch",
6 | description:
7 | "QSwitch is a switch-like checkbox which offers binary choices. It supports labels, icons and different positioning of the labels.",
8 | docs: {
9 | props: QSwitchDocsProps,
10 | snippets: QSwitchDocsSnippets,
11 | methods: [],
12 | events: [
13 | {
14 | name: "input",
15 | type: "(value: boolean) => void",
16 | description: "Emitted when the user changes the value of the toggle.",
17 | },
18 | ],
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/src/lib/css/mixins/_table.scss:
--------------------------------------------------------------------------------
1 | @mixin table {
2 | width: 100%;
3 | border-spacing: 0;
4 | font-size: 0.875rem;
5 | color: var(--on-background);
6 | text-align: left;
7 | border-radius: 0;
8 |
9 | :is(th, td) {
10 | width: 1%;
11 | text-align: inherit;
12 | // from &--md, as default, it was 0.5rem prior
13 | padding: 0.75rem;
14 | }
15 |
16 | th {
17 | font-weight: 500;
18 | }
19 |
20 | :is(th, td) {
21 | border-bottom: 0.0625rem solid var(--outline);
22 | }
23 |
24 | &--sm,
25 | &--dense :is(th, td) {
26 | padding: 0;
27 | }
28 |
29 | &--md :is(th, td) {
30 | padding: 0.75rem;
31 | }
32 |
33 | &--lg :is(th, td) {
34 | padding: 1rem;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/lib/components/card/QCardActions.svelte:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 | {@render children?.()}
23 |
24 |
--------------------------------------------------------------------------------
/docgen/snippets/getSnippetPagePaths.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import { fileURLToPath } from "url";
3 | import getComponentDirs from "../helpers/getComponentDirs.js";
4 |
5 | const dirname = path.dirname(fileURLToPath(import.meta.url));
6 | const rootDir = path.resolve(dirname, "../../src/routes/components");
7 |
8 | export default async function getSnippetPagePaths() {
9 | const componentsToIgnore = ["layout", "private"];
10 |
11 | const componentDirs = (await getComponentDirs(rootDir)).filter(
12 | (dir) => !componentsToIgnore.includes(dir)
13 | );
14 |
15 | return componentDirs.map((dir) => {
16 | const dirPath = path.resolve(rootDir, dir);
17 | return path.resolve(dirPath, "+page.svelte");
18 | });
19 | }
20 |
--------------------------------------------------------------------------------
/src/lib/components/breadcrumbs/QBreadcrumbsEl.scss:
--------------------------------------------------------------------------------
1 | @use "$css/mixins";
2 |
3 | .q-breadcrumbs__item {
4 | display: flex;
5 | align-items: center;
6 |
7 | color: var(--q-breadcrumbs-color, currentColor);
8 |
9 | &:has(> a):hover .q-breadcrumbs__label {
10 | text-decoration: underline;
11 | text-decoration-color: currentColor;
12 |
13 | &__item:first-child > .q-breadcrumbs__separator {
14 | display: none;
15 | }
16 | }
17 |
18 | .q-breadcrumbs__separator {
19 | color: var(--q-separator-color, currentColor);
20 | }
21 |
22 | .q-breadcrumbs__el {
23 | display: flex;
24 | align-items: center;
25 | gap: 0.5rem;
26 | text-decoration: none;
27 | color: inherit;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/lib/components/chip/docs.ts:
--------------------------------------------------------------------------------
1 | import type { QComponentDocs } from "$utils";
2 | import { QChipDocsProps, QChipDocsSnippets } from "./docs.props";
3 |
4 | export const QChipDocs: QComponentDocs = {
5 | name: "QChip",
6 | description:
7 | "Chips help people enter information, make selections, filter content, or trigger actions. They represent options in a specific context, unlike buttons, which are persistent.",
8 | docs: {
9 | props: QChipDocsProps,
10 | snippets: QChipDocsSnippets,
11 | methods: [],
12 | events: [
13 | {
14 | name: "click",
15 | type: "(e: MouseEvent) => void",
16 | description: "Emitted when the user clicks on the chip.",
17 | },
18 | ],
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/src/lib/components/expansion-item/docs.ts:
--------------------------------------------------------------------------------
1 | import type { QComponentDocs } from "$utils";
2 | import { QExpansionItemDocsProps, QExpansionItemDocsSnippets } from "./docs.props";
3 |
4 | export const QExpansionItemDocs: QComponentDocs = {
5 | name: "QExpansionItem",
6 | description:
7 | "The QExpansionItem component allows users to create expandable/collapsible sections within a list or a card.",
8 | docs: {
9 | props: QExpansionItemDocsProps,
10 | snippets: QExpansionItemDocsSnippets,
11 | methods: [],
12 | events: [
13 | {
14 | name: "click",
15 | type: "(e: MouseEvent) => void",
16 | description: "Emitted when the user clicks on the expansion item.",
17 | },
18 | ],
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/src/lib/components/avatar/docs.ts:
--------------------------------------------------------------------------------
1 | import type { QComponentDocs } from "$utils";
2 | import { QAvatarDocsProps, QAvatarDocsSnippets } from "./docs.props";
3 |
4 | export const QAvatarDocs: QComponentDocs = {
5 | name: "QAvatar",
6 | description:
7 | "Avatars can be used in many different ways as with icons or for user profile images/videos, for example. They can have many different shapes, the default one being a circle.",
8 | docs: {
9 | props: QAvatarDocsProps,
10 | snippets: QAvatarDocsSnippets,
11 | methods: [],
12 | events: [
13 | {
14 | name: "click",
15 | type: "(e: MouseEvent) => void",
16 | description: "Emitted when the user clicks on the avatar.",
17 | },
18 | ],
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/src/lib/components/toolbar/QToolbar.scss:
--------------------------------------------------------------------------------
1 | @use "$css/mixins";
2 | @use "$css/variables";
3 |
4 | .q-toolbar {
5 | @include mixins.toolbarDisplay;
6 |
7 | width: 100%;
8 | height: var(--toolbar-height);
9 | padding: 0 1rem;
10 | transition: all variables.$speed3;
11 | background-color: var(--surface);
12 |
13 | &--inset {
14 | padding-left: 3.625rem;
15 | }
16 |
17 | &--elevated {
18 | @include mixins.elevate(1, "bottom");
19 | }
20 |
21 | &--bordered {
22 | @include mixins.border($position: "bottom");
23 | }
24 | }
25 |
26 | .q-toolbar-title {
27 | display: flex;
28 | flex: auto;
29 | font-size: 1.75rem;
30 | font-weight: 400;
31 | justify-content: center;
32 |
33 | &--shrink {
34 | flex: unset;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/lib/components/input/docs.ts:
--------------------------------------------------------------------------------
1 | import type { QComponentDocs } from "$utils";
2 | import { QInputDocsProps, QInputDocsSnippets } from "./docs.props";
3 |
4 | export const QInputDocs: QComponentDocs = {
5 | name: "QInput",
6 | description:
7 | "QInput is a form component that allows users to input text. It supports different visual styles such as filled, outlined, and rounded, and it can also display hint text and custom error messages.",
8 | docs: {
9 | props: QInputDocsProps,
10 | snippets: QInputDocsSnippets,
11 | methods: [],
12 | events: [
13 | {
14 | name: "input",
15 | type: "(value: string) => void",
16 | description: "Emitted when the user types in the input component.",
17 | },
18 | ],
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/src/lib/components/select/docs.ts:
--------------------------------------------------------------------------------
1 | import type { QComponentDocs } from "$utils";
2 | import { QSelectDocsProps, QSelectDocsSnippets } from "./docs.props";
3 |
4 | export const QSelectDocs: QComponentDocs = {
5 | name: "QSelect",
6 | description:
7 | "QSelect is a form component that allows users to choose from multiple options in a dropdown list. It supports single and multiple selection, as well as different visual styles such as filled, outlined, and rounded.",
8 | docs: {
9 | props: QSelectDocsProps,
10 | snippets: QSelectDocsSnippets,
11 | methods: [],
12 | events: [
13 | {
14 | name: "change",
15 | type: "(value: string | string[]) => void",
16 | description: "Emitted when the value of the select component changes.",
17 | },
18 | ],
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/src/lib/components/tabs/docs.ts:
--------------------------------------------------------------------------------
1 | import type { QComponentDocs } from "$utils";
2 | import { QTabDocsProps, QTabDocsSnippets, QTabsDocsProps, QTabsDocsSnippets } from "./docs.props";
3 |
4 | export const QTabsDocs: QComponentDocs = {
5 | name: "QTabs",
6 | description:
7 | "Tabs allow creating navigational tabs, enabling users to switch between different views or functional aspects.",
8 | docs: {
9 | props: QTabsDocsProps,
10 | snippets: QTabsDocsSnippets,
11 | methods: [],
12 | events: [],
13 | },
14 | };
15 |
16 | export const QTabDocs: QComponentDocs = {
17 | name: "QTabs",
18 | description:
19 | "Tabs allow creating navigational tabs, enabling users to switch between different views or functional aspects.",
20 | docs: {
21 | props: QTabDocsProps,
22 | snippets: QTabDocsSnippets,
23 | methods: [],
24 | events: [],
25 | },
26 | };
27 |
--------------------------------------------------------------------------------
/src/lib/components/footer/QFooter.scss:
--------------------------------------------------------------------------------
1 | @use "$css/mixins";
2 | @use "$css/variables";
3 |
4 | .q-footer {
5 | z-index: 4;
6 | position: absolute;
7 | top: auto;
8 | right: 0;
9 | bottom: 0;
10 | left: 0;
11 |
12 | @include mixins.toolbarDisplay;
13 |
14 | @include mixins.padding("x-md");
15 |
16 | min-height: unset;
17 | height: var(--footer-height);
18 | width: auto;
19 |
20 | border-radius: 0;
21 |
22 | background-color: var(--surface);
23 |
24 | &.q-footer--bordered {
25 | @include mixins.border(var(--outline), "top");
26 | }
27 |
28 | &.q-footer--collapsed {
29 | translate: 0 calc(1.5 * var(--footer-height));
30 | }
31 |
32 | & > nav {
33 | height: 100%;
34 | min-height: unset;
35 | }
36 |
37 | @each $side in ("left", "right") {
38 | &.q-footer--offset-#{$side} {
39 | #{$side}: var(--offset-#{$side}, 0);
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/lib/components/radio/props.ts:
--------------------------------------------------------------------------------
1 | import type { HTMLAttributes } from "svelte/elements";
2 |
3 | export interface QRadioProps extends HTMLAttributes {
4 | /**
5 | * Value associated with this radio button. Used when comparing against the selected value.
6 | *
7 | * @default ""
8 | */
9 | value: string;
10 |
11 | /**
12 | * Text label displayed next to the radio button.
13 | *
14 | * @default ""
15 | */
16 | label?: string;
17 |
18 | /**
19 | * Bound value that determines if this radio button is selected. This prop is bindable.
20 | * When using a group of radio buttons, this should be the same variable for all of them.
21 | *
22 | * @default undefined
23 | */
24 | selected?: unknown;
25 |
26 | /**
27 | * When true, prevents user interaction with the radio button.
28 | *
29 | * @default false
30 | */
31 | disable?: boolean;
32 | }
33 |
--------------------------------------------------------------------------------
/src/lib/components/private/QIconSnippet.svelte:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 | {#if typeof iconToUse === "string"}
19 |
20 | {:else}
21 | {@render iconToUse?.()}
22 | {/if}
23 |
--------------------------------------------------------------------------------
/.github/workflows/npmpublish.yml:
--------------------------------------------------------------------------------
1 | name: Publish Package to npmjs
2 | on:
3 | workflow_dispatch:
4 | inputs:
5 | version:
6 | description: "version (e.g. 1.2.3)"
7 | required: true
8 |
9 | jobs:
10 | publish:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v5
14 |
15 | - uses: oven-sh/setup-bun@v2
16 | with:
17 | bun-version: 1.3.1
18 | registry-url: "https://registry.npmjs.org"
19 |
20 | - name: Set version in package.json
21 | run: |
22 | jq --arg v "${{ github.event.inputs.version }}" '.version = $v' package.json > tmp && mv tmp package.json
23 |
24 | - name: Publish to npm
25 | run: |
26 | echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc
27 | bun install --frozen-lockfile
28 | bun gen:plugins
29 | bun publish --access public
30 |
--------------------------------------------------------------------------------
/docgen/props/updateDocTypesFile.ts:
--------------------------------------------------------------------------------
1 | import { readFile, writeFile } from "fs/promises";
2 |
3 | export default async function updateDocTypesFile(
4 | docTypesPath: string,
5 | newTypes: Record,
6 | didUpdateAllFiles: boolean
7 | ) {
8 | let oldTypes: Record = {};
9 |
10 | // if only a few files were updated, we need to build on the existing types data
11 | if (!didUpdateAllFiles) {
12 | const oldTypesStr = await readFile(docTypesPath, "utf8");
13 | oldTypes = JSON.parse(oldTypesStr);
14 | }
15 |
16 | const allTypesRaw = Object.assign(oldTypes, newTypes);
17 |
18 | // sort alphabetically
19 | const allTypesSorted = Object.fromEntries(
20 | Object.entries(allTypesRaw).sort((a, b) => a[0].localeCompare(b[0]))
21 | );
22 |
23 | const allTypesStr = JSON.stringify(allTypesSorted, null, 2) + "\n";
24 |
25 | await writeFile(docTypesPath, allTypesStr, "utf8");
26 | }
27 |
--------------------------------------------------------------------------------
/src/lib/components/codeBlock/props.ts:
--------------------------------------------------------------------------------
1 | import type { HTMLAttributes } from "svelte/elements";
2 | import type { SpecialLanguage, BundledLanguage, BundledTheme } from "shiki";
3 |
4 | export interface QCodeBlockProps extends HTMLAttributes {
5 | /**
6 | * Language to use for highlighting.
7 | */
8 | language: BundledLanguage | SpecialLanguage;
9 |
10 | /**
11 | * Theme to use for highlighting for light mode.
12 | */
13 | lightTheme?: BundledTheme;
14 |
15 | /**
16 | * Theme to use for highlighting for dark mode.
17 | */
18 | darkTheme?: BundledTheme;
19 |
20 | /**
21 | * Code to highlight.
22 | */
23 | code?: string;
24 |
25 | /**
26 | * Title to display above the code.
27 | * @default undefined
28 | */
29 | title?: string;
30 |
31 | /**
32 | * Wether the code should be copiable or not.
33 | * @default false
34 | */
35 | copiable?: boolean;
36 | }
37 |
--------------------------------------------------------------------------------
/src/lib/utils/router.ts:
--------------------------------------------------------------------------------
1 | import { page } from "$app/state";
2 |
3 | export interface RouterProps {
4 | activeClass?: string;
5 | disabled?: boolean;
6 | href?: string;
7 | replace?: boolean;
8 | to?: string;
9 | }
10 |
11 | export function isRouteActive(route?: string) {
12 | if (!route) {
13 | return false;
14 | }
15 |
16 | return page.url.pathname === route || page.url.pathname.startsWith(`${route}/`);
17 | }
18 |
19 | export function getRouterInfo(props: T) {
20 | const hasLink = [props.to, props.href].some((entry) => entry !== undefined);
21 | const linkClasses = `${hasLink ? "q-link" : ""} ${props.disabled ? "disabled" : ""}`.trim();
22 |
23 | const linkAttributes = {
24 | href: props.to || props.href,
25 | "data-sveltekit-reload": props.replace ? true : undefined,
26 | };
27 |
28 | return {
29 | hasLink,
30 | linkAttributes,
31 | linkClasses,
32 | };
33 | }
34 |
--------------------------------------------------------------------------------
/src/lib/css/classes/_grid.scss:
--------------------------------------------------------------------------------
1 | @use "../variables";
2 | @use "sass:map";
3 |
4 | .row {
5 | display: grid;
6 | grid-template-columns: repeat(variables.$grid-columns, 1fr);
7 | grid-gap: 0;
8 |
9 | // Gutter classes
10 | @each $spaceName, $spaceVal in variables.$spaces {
11 | @if $spaceName != "auto" {
12 | &.q-gutter-#{$spaceName} {
13 | gap: $spaceVal;
14 | }
15 | }
16 | }
17 |
18 | // Column classes for all sizes
19 | @for $i from 1 through variables.$grid-columns {
20 | & > .col-#{$i} {
21 | grid-column: span #{$i};
22 | }
23 | }
24 |
25 | // Column classes for each breakpoint
26 | @each $breakpoint, $min-width in variables.$breakpoints {
27 | @media (min-width: #{$min-width}) {
28 | @for $i from 1 through variables.$grid-columns {
29 | & > .col-#{$breakpoint}-#{$i} {
30 | grid-column: span #{$i};
31 | }
32 | }
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/lib/components/tooltip/QTooltip.scss:
--------------------------------------------------------------------------------
1 | @use "$css/variables";
2 |
3 | .q-tooltip {
4 | --space: -0.5rem;
5 | display: flex;
6 | align-items: center;
7 | justify-content: center;
8 | cursor: auto;
9 | gap: 0.5rem;
10 | background-color: var(--inverse-surface);
11 | color: var(--inverse-on-surface);
12 | font-size: 0.75rem;
13 | text-align: center;
14 | border-radius: 0.25rem;
15 | padding: 0.5rem;
16 | position: fixed;
17 | bottom: auto;
18 | right: auto;
19 | width: auto;
20 | white-space: nowrap;
21 | font-weight: 500;
22 | transition: opacity variables.$speed2;
23 | z-index: 9999;
24 |
25 | &__helper {
26 | // Remove the helper element from the flow so it doesn't affect pre elements
27 | position: absolute;
28 | }
29 |
30 | &:has(> .q-card) {
31 | padding: 0;
32 | overflow: hidden;
33 | background-color: transparent;
34 | text-align: unset;
35 |
36 | & > .q-card {
37 | box-shadow: none;
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/lib/components/toolbar/docs.ts:
--------------------------------------------------------------------------------
1 | import type { QComponentDocs } from "$utils";
2 | import {
3 | QToolbarDocsProps,
4 | QToolbarDocsSnippets,
5 | QToolbarTitleDocsProps,
6 | QToolbarTitleDocsSnippets,
7 | } from "./docs.props";
8 |
9 | export const QToolbarDocs: QComponentDocs = {
10 | name: "QToolbar",
11 | description:
12 | "The Toolbar component is used to hold common actions and controls, often located at the top of an application or view.",
13 | docs: {
14 | props: QToolbarDocsProps,
15 | snippets: QToolbarDocsSnippets,
16 | methods: [],
17 | events: [],
18 | },
19 | };
20 |
21 | export const QToolbarTitleDocs: QComponentDocs = {
22 | name: "QToolbar",
23 | description:
24 | "The Toolbar component is used to hold common actions and controls, often located at the top of an application or view.",
25 | docs: {
26 | props: QToolbarTitleDocsProps,
27 | snippets: QToolbarTitleDocsSnippets,
28 | methods: [],
29 | events: [],
30 | },
31 | };
32 |
--------------------------------------------------------------------------------
/src/lib/components/toolbar/props.ts:
--------------------------------------------------------------------------------
1 | import type { HTMLAttributes } from "svelte/elements";
2 |
3 | export interface QToolbarProps extends HTMLAttributes {
4 | /**
5 | * Adds horizontal padding to the toolbar content.
6 | *
7 | * @default false
8 | */
9 | inset?: boolean;
10 |
11 | /**
12 | * Adds a border to the toolbar to separate it from the main content.
13 | *
14 | * @default false
15 | */
16 | border?: boolean;
17 |
18 | /**
19 | * Adds a shadow to the toolbar to make it appear elevated.
20 | *
21 | * @default false
22 | */
23 | elevate?: boolean;
24 |
25 | /**
26 | * Height of the toolbar in pixels.
27 | *
28 | * @default 64
29 | */
30 | height?: number;
31 | }
32 |
33 | export interface QToolbarTitleProps extends HTMLAttributes {
34 | /**
35 | * Allows the title to shrink when there isn't enough space in the toolbar.
36 | *
37 | * @default false
38 | */
39 | shrink?: boolean;
40 | }
41 |
--------------------------------------------------------------------------------
/src/lib/css/theme/_reset.scss:
--------------------------------------------------------------------------------
1 | * {
2 | -webkit-tap-highlight-color: transparent;
3 | position: relative;
4 | vertical-align: middle;
5 | color: inherit;
6 | margin: 0;
7 | padding: 0;
8 | border-radius: inherit;
9 | box-sizing: border-box;
10 | }
11 |
12 | html,
13 | body {
14 | min-height: 100%;
15 | width: 100%;
16 | color: var(--on-surface);
17 | background-color: var(--surface);
18 | }
19 |
20 | body,
21 | body > div:not(.q-tooltip) {
22 | background-color: var(--surface-container-lowest);
23 | overflow-x: hidden;
24 | height: 100vh;
25 | width: 100%;
26 | }
27 |
28 | label {
29 | font-size: 0.75rem;
30 | vertical-align: baseline;
31 | }
32 |
33 | a,
34 | b,
35 | i,
36 | span {
37 | vertical-align: bottom;
38 | }
39 |
40 | a,
41 | button {
42 | cursor: pointer;
43 | text-decoration: none;
44 | display: inline-flex;
45 | align-items: center;
46 | border: none;
47 | font-family: inherit;
48 | outline: inherit;
49 | justify-content: center;
50 | }
51 |
--------------------------------------------------------------------------------
/docgen/snippets/updateSnippetsForPage.ts:
--------------------------------------------------------------------------------
1 | import { writeFile } from "fs/promises";
2 | import path from "path";
3 | import formatCodeAndAddHash from "../helpers/formatCodeAndAddHash.js";
4 | import getInfo from "./getInfo.js";
5 |
6 | export default async function updateSnippetsForPage(pageFilePath: string) {
7 | const dirPath = path.dirname(pageFilePath);
8 | const docsSnippetsFilePath = path.resolve(dirPath, "docs.snippets.ts");
9 |
10 | const snippets: Record = {};
11 | const { needsToBeGenerated, hashSections, sections } = await getInfo(
12 | pageFilePath,
13 | docsSnippetsFilePath
14 | );
15 |
16 | if (needsToBeGenerated && sections) {
17 | sections.forEach(({ title, content }) => {
18 | snippets[title] = content;
19 | });
20 |
21 | const code = `export default ${JSON.stringify(snippets)}`;
22 | const formattedCode = await formatCodeAndAddHash(code, hashSections!);
23 |
24 | await writeFile(docsSnippetsFilePath, formattedCode, "utf8");
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/lib/components/expansion-item/QExpansionItem.scss:
--------------------------------------------------------------------------------
1 | @use "$css/mixins";
2 | @use "$css/variables";
3 |
4 | .q-expansion-item {
5 | width: 100%;
6 | padding: 0;
7 |
8 | &[aria-disabled] :is(.q-item__section, .q-item__section *) {
9 | color: unset;
10 | }
11 |
12 | & summary::marker,
13 | & summary::-webkit-details-marker {
14 | display: none;
15 | content: "";
16 | }
17 |
18 | &::details-content {
19 | transition: content-visibility var(--duration) allow-discrete;
20 | }
21 |
22 | &__toggle-icon {
23 | margin: 0;
24 | z-index: 1;
25 |
26 | &:is(.q-btn) {
27 | margin-right: -0.5rem;
28 | }
29 |
30 | &:not(.q-btn),
31 | &.q-btn .q-icon {
32 | transition: rotate var(--duration);
33 | }
34 |
35 | &--rotate:not(.q-btn),
36 | &--rotate.q-btn .q-icon {
37 | rotate: 180deg;
38 | }
39 | }
40 |
41 | &__content {
42 | @include mixins.padding("x-md", "y-sm");
43 | width: 100%;
44 | overflow: hidden;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/lib/css/classes/_design.scss:
--------------------------------------------------------------------------------
1 | @use "$css/disabled";
2 | @use "../mixins" as *;
3 |
4 | // Disabled
5 | .disabled {
6 | @include disabled.base();
7 | }
8 |
9 | // Borders
10 | .border {
11 | @include border;
12 | }
13 |
14 | .border-top {
15 | @include border(var(--outline), top);
16 | }
17 |
18 | .border-right {
19 | @include border(var(--outline), right);
20 | }
21 |
22 | .border-bottom {
23 | @include border(var(--outline), bottom);
24 | }
25 |
26 | .border-left {
27 | @include border(var(--outline), left);
28 | }
29 |
30 | // Elevation
31 | .elevate {
32 | &-none {
33 | box-shadow: none;
34 | }
35 |
36 | $elevations: (
37 | "sm": 1,
38 | "md": 2,
39 | "lg": 3,
40 | );
41 |
42 | @each $elevation, $value in $elevations {
43 | &-#{$elevation} {
44 | &-top {
45 | @include elevate($value, "top");
46 | }
47 | &-bottom {
48 | @include elevate($value, "bottom");
49 | }
50 | }
51 | }
52 | }
53 |
54 | // Transparent
55 | .transparent {
56 | @include transparent;
57 | }
58 |
--------------------------------------------------------------------------------
/src/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %sveltekit.head%
8 |
9 |
10 |
27 |
28 | %sveltekit.body%
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/lib/css/_ripple.scss:
--------------------------------------------------------------------------------
1 | .q-ripple {
2 | background-color: var(--ripple-color, var(--outline));
3 | opacity: 0.1;
4 | position: absolute;
5 | border-radius: 50%;
6 | pointer-events: none;
7 | -webkit-transition: 0.6s;
8 | transition: 0.6s;
9 | -webkit-animation: ripple var(--ripple-duration, 0.4s) cubic-bezier(0.4, 0, 0.2, 1);
10 | animation: ripple var(--ripple-duration, 0.4s) cubic-bezier(0.4, 0, 0.2, 1);
11 |
12 | &--center .q-ripple {
13 | top: 50% !important;
14 | left: 50% !important;
15 | translate: -50% -50% !important;
16 | }
17 |
18 | &--effect {
19 | position: absolute;
20 | left: 0;
21 | right: 0;
22 | top: 0;
23 | bottom: 0;
24 | overflow: hidden;
25 | background: none;
26 | pointer-events: none;
27 | z-index: 999;
28 | }
29 | }
30 |
31 | @-webkit-keyframes ripple {
32 | from {
33 | scale: 0;
34 | }
35 |
36 | to {
37 | scale: 1;
38 | }
39 | }
40 |
41 | @keyframes ripple {
42 | from {
43 | scale: 0;
44 | }
45 |
46 | to {
47 | scale: 1;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/lib/composables/useSize.ts:
--------------------------------------------------------------------------------
1 | import { isNumeric } from "$utils";
2 |
3 | export const sizes: Q.Size[] = ["none", "xs", "sm", "md", "lg", "xl"];
4 |
5 | export const CssUnits: Q.CssUnit[] = [
6 | "px",
7 | "%",
8 | "em",
9 | "ex",
10 | "ch",
11 | "rem",
12 | "vw",
13 | "vh",
14 | "vmin",
15 | "vmax",
16 | ];
17 |
18 | /**
19 | * Checks wether the input is a size like "sm" or "lg"
20 | */
21 | export function isQuaffSize(size: number | string): size is Q.Size {
22 | return sizes.includes(size as Q.Size);
23 | }
24 |
25 | export function useSize(size: number | string, component?: `q-${string}`) {
26 | const sizeClass = isQuaffSize(size) ? ` ${component}--${size}` : "";
27 | const sizeStyle = () => {
28 | if (isNumeric(size)) {
29 | return +size > 0 ? `${size}px` : undefined;
30 | }
31 |
32 | for (const unit of CssUnits) {
33 | if (size.slice(-unit.length) === unit) {
34 | return size;
35 | }
36 | }
37 | };
38 |
39 | return {
40 | class: sizeClass,
41 | style: sizeStyle(),
42 | };
43 | }
44 |
--------------------------------------------------------------------------------
/src/lib/components/footer/props.ts:
--------------------------------------------------------------------------------
1 | import type { NativeProps } from "$utils";
2 | import type { HTMLAttributes } from "svelte/elements";
3 |
4 | export interface QFooterProps extends NativeProps, HTMLAttributes {
5 | /**
6 | * The value indicating whether the footer is visible or hidden.
7 | * @default true
8 | */
9 | value?: boolean;
10 |
11 | /**
12 | * Determines whether the footer has a top border.
13 | * @default false
14 | */
15 | bordered?: boolean;
16 |
17 | /**
18 | * Determines whether the footer should hide on scroll.
19 | * @default false
20 | */
21 | reveal?: boolean;
22 |
23 | /**
24 | * The offset in pixels to trigger the reveal effect. The footer will be hidden when the scroll position is greater than this value.
25 | * @default 250
26 | */
27 | revealOffset?: number;
28 |
29 | /**
30 | * The height of the footer. Can be specified with a CSS unit. If not specified, "px" will be used. (specified CSS units are not supported yet)
31 | * @default undefined
32 | */
33 | height?: number;
34 | }
35 |
--------------------------------------------------------------------------------
/src/lib/components/progress/docs.ts:
--------------------------------------------------------------------------------
1 | import type { QComponentDocs } from "$utils";
2 | import {
3 | QLinearProgressDocsProps,
4 | QLinearProgressDocsSnippets,
5 | QCircularProgressDocsProps,
6 | QCircularProgressDocsSnippets,
7 | } from "./docs.props";
8 |
9 | export const QLinearProgressDocs: QComponentDocs = {
10 | name: "QLinearProgress",
11 | description:
12 | "The QLinearProgress component is used to display a progress bar, indicating the completion status of a task or process.",
13 | docs: {
14 | props: QLinearProgressDocsProps,
15 | snippets: QLinearProgressDocsSnippets,
16 | methods: [],
17 | events: [],
18 | },
19 | };
20 |
21 | export const QCircularProgressDocs: QComponentDocs = {
22 | name: "QCircularProgress",
23 | description:
24 | "The QCircularProgress component is used to display a circular progress bar, indicating the completion status of a task or process.",
25 | docs: {
26 | props: QCircularProgressDocsProps,
27 | snippets: QCircularProgressDocsSnippets,
28 | methods: [],
29 | events: [],
30 | },
31 | };
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright © 2025 QuaffUI
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/src/lib/components/dialog/props.ts:
--------------------------------------------------------------------------------
1 | import type { NativeProps } from "$utils";
2 | import type { HTMLAttributes } from "svelte/elements";
3 |
4 | export type QDialogPositionOptions = "default" | "top" | "right" | "bottom" | "left";
5 |
6 | export interface QDialogProps extends NativeProps, HTMLAttributes {
7 | /**
8 | * The value indicating whether the dialog is visible or hidden.
9 | * @default true
10 | */
11 | value?: boolean;
12 |
13 | /**
14 | * The position of the dialog relative to the viewport.
15 | * @default "default"
16 | */
17 | position?: QDialogPositionOptions;
18 |
19 | /**
20 | * Determines whether the dialog is displayed as a modal or not.
21 | * @default false
22 | */
23 | modal?: boolean;
24 |
25 | /**
26 | * Determines whether the dialog is displayed in fullscreen mode.
27 | * @default false
28 | */
29 | fullscreen?: boolean;
30 |
31 | /**
32 | * Determines whether the dialog remains persistent, not closing on click outside.
33 | * @default false
34 | */
35 | persistent?: boolean;
36 | }
37 |
--------------------------------------------------------------------------------
/src/lib/css/classes/_flex.scss:
--------------------------------------------------------------------------------
1 | .flex {
2 | display: flex !important;
3 | flex-wrap: wrap;
4 | }
5 |
6 | .column {
7 | flex-direction: column;
8 | }
9 |
10 | // Horizontal alignment
11 | .items-start {
12 | align-items: flex-start;
13 | }
14 |
15 | .items-center {
16 | align-items: center;
17 | }
18 |
19 | .items-end {
20 | align-items: flex-end;
21 | }
22 |
23 | // Vertical alignment
24 | .justify-start {
25 | justify-content: flex-start;
26 | }
27 |
28 | .justify-center {
29 | justify-content: center;
30 | }
31 |
32 | .justify-end {
33 | justify-content: flex-end;
34 | }
35 |
36 | .justify-between {
37 | justify-content: space-between;
38 | }
39 |
40 | .justify-around {
41 | justify-content: space-around;
42 | }
43 |
44 | .justify-evenly {
45 | justify-content: space-evenly;
46 | }
47 |
48 | // Shorthands
49 | .flex-start {
50 | @extend .items-start;
51 | @extend .justify-start;
52 | }
53 |
54 | .flex-center {
55 | @extend .items-center;
56 | @extend .justify-center;
57 | }
58 |
59 | .flex-end {
60 | @extend .items-end;
61 | @extend .justify-end;
62 | }
63 |
--------------------------------------------------------------------------------
/src/lib/components/header/QHeader.scss:
--------------------------------------------------------------------------------
1 | @use "$css/mixins";
2 | @use "$css/variables";
3 |
4 | .q-header {
5 | z-index: 4;
6 | position: absolute;
7 | top: 0;
8 | right: 0;
9 | bottom: auto;
10 | left: 0;
11 |
12 | @include mixins.toolbarDisplay;
13 |
14 | min-height: unset;
15 | height: var(--header-height);
16 | width: auto;
17 |
18 | @include mixins.padding("x-md");
19 |
20 | border-radius: 0;
21 |
22 | background-color: var(--surface);
23 |
24 | &--inset {
25 | padding-left: 3.625rem;
26 | }
27 |
28 | &.q-header--elevated {
29 | @include mixins.elevate(1);
30 | }
31 |
32 | &.q-header--bordered {
33 | @include mixins.border(var(--outline), "bottom");
34 | }
35 |
36 | &.q-header--dense {
37 | --header-height: 3rem;
38 | }
39 |
40 | &.q-header--prominent {
41 | --header-height: 7rem;
42 | }
43 |
44 | &.q-header--collapsed {
45 | translate: 0 calc(-1.5 * var(--header-height));
46 | }
47 |
48 | @each $side in ("left", "right") {
49 | &.q-header--offset-#{$side} {
50 | #{$side}: var(--offset-#{$side}, 0);
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/docgen/snippets/parseSvelteFile.ts:
--------------------------------------------------------------------------------
1 | import { readFile } from "fs/promises";
2 | import { format } from "prettier";
3 |
4 | export type SnippetSection = {
5 | title: string;
6 | content: string;
7 | };
8 |
9 | export default async function parseSvelteFile(svelteFilePath: string) {
10 | const fileContent = await readFile(svelteFilePath, "utf-8");
11 | const qDocsSectionPattern =
12 | /]*?title="(.*?)"\s*(?:noCode)?\s*>(.*?)<\/QDocsSection>/gms;
13 |
14 | let match;
15 | const sections: SnippetSection[] = [];
16 |
17 | while ((match = qDocsSectionPattern.exec(fileContent))) {
18 | const title = match[1];
19 | const content = match[2].trim();
20 |
21 | const contentWithoutDescription = content.replace(
22 | /\{#snippet sectionDescription\(\)\}.+?\s{8}\{\/snippet\}/gms,
23 | ""
24 | );
25 |
26 | const formatted = await format(contentWithoutDescription, {
27 | parser: "svelte",
28 | plugins: ["prettier-plugin-svelte"],
29 | printWidth: 100,
30 | });
31 |
32 | sections.push({ title, content: formatted });
33 | }
34 |
35 | return sections;
36 | }
37 |
--------------------------------------------------------------------------------
/src/lib/components/separator/docs.ts:
--------------------------------------------------------------------------------
1 | import type { QComponentDocs } from "$utils";
2 | import {
3 | QSeparatorVerticalDocsSnippets,
4 | QSeparatorVerticalDocsProps,
5 | QSeparatorHorizontalDocsProps,
6 | QSeparatorHorizontalDocsSnippets,
7 | } from "./docs.props";
8 |
9 | export const QSeparatorVerticalDocs: QComponentDocs = {
10 | name: "QSeparator (Vertical)",
11 | description:
12 | "Separators can be used to create a dividing line or space between elements within a layout, offering visual separation and organization.",
13 | docs: {
14 | props: QSeparatorVerticalDocsProps,
15 | snippets: QSeparatorVerticalDocsSnippets,
16 | methods: [],
17 | events: [],
18 | },
19 | };
20 |
21 | export const QSeparatorHorizontalDocs: QComponentDocs = {
22 | name: "QSeparator",
23 | description:
24 | "Separators can be used to create a dividing line or space between elements within a layout, offering visual separation and organization.",
25 | docs: {
26 | props: QSeparatorHorizontalDocsProps,
27 | snippets: QSeparatorHorizontalDocsSnippets,
28 | methods: [],
29 | events: [],
30 | },
31 | };
32 |
--------------------------------------------------------------------------------
/docgen/props/getInfo.ts:
--------------------------------------------------------------------------------
1 | import { readFile } from "fs/promises";
2 | import pathExists from "../helpers/pathExists.js";
3 | import generateHash from "../helpers/generateHash.js";
4 |
5 | function extractHash(docsPropsContent: string) {
6 | const hashPattern = /@quaffHash ([a-f0-9]+)/;
7 | const match = docsPropsContent.match(hashPattern);
8 |
9 | return match?.[1] || null;
10 | }
11 |
12 | type InfoObject = {
13 | needsToBeGenerated: boolean;
14 | hashProps?: string;
15 | };
16 |
17 | export default async function getInfo(
18 | propsFilePath: string,
19 | docsPropsFilePath: string
20 | ): Promise {
21 | if (!(await pathExists(propsFilePath))) {
22 | return { needsToBeGenerated: false };
23 | }
24 |
25 | const hashProps = generateHash(await readFile(propsFilePath, "utf8"));
26 |
27 | if (!(await pathExists(docsPropsFilePath))) {
28 | return { needsToBeGenerated: true, hashProps };
29 | }
30 |
31 | const hashFromDocsProps = extractHash(await readFile(docsPropsFilePath, "utf8"));
32 | const needsToBeGenerated = hashProps !== hashFromDocsProps;
33 |
34 | return { needsToBeGenerated, hashProps };
35 | }
36 |
--------------------------------------------------------------------------------
/svelte.config.js:
--------------------------------------------------------------------------------
1 | import adapter from "@sveltejs/adapter-static";
2 | import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
3 | import { preprocessClasses, preprocessContext } from "./plugins/dist/index.js";
4 |
5 | const NAMESPACE = "Q";
6 |
7 | /** @type {import('@sveltejs/kit').Config} */
8 | const config = {
9 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors
10 | // for more information about preprocessors
11 | preprocess: [preprocessContext(), vitePreprocess(), preprocessClasses(NAMESPACE)],
12 |
13 | kit: {
14 | adapter: adapter({
15 | pages: "build",
16 | assets: "build",
17 | fallback: "404.html",
18 | precompress: true,
19 | }),
20 |
21 | prerender: { entries: ["*"] },
22 |
23 | alias: {
24 | $docgen: "./docgen",
25 | $components: "./src/lib/components",
26 | $composables: "./src/lib/composables",
27 | $utils: "./src/lib/utils",
28 | $css: "./src/lib/css",
29 | $stores: "./src/lib/stores",
30 | $helpers: "./src/lib/helpers",
31 | $private: "./src/lib/components/private",
32 | },
33 | },
34 | };
35 |
36 | export default config;
37 |
--------------------------------------------------------------------------------
/src/lib/components/card/QCard.scss:
--------------------------------------------------------------------------------
1 | @use "$css/mixins";
2 |
3 | .q-card {
4 | display: block;
5 | border-radius: 0.75rem;
6 | transition: transform var(--speed-3) padding var(--speed-3) border-radius var(--speed-3);
7 |
8 | background-color: var(--surface);
9 | color: var(--on-surface);
10 |
11 | @include mixins.padding("a-md");
12 | @include mixins.elevate(1, "bottom");
13 |
14 | &--bordered {
15 | box-sizing: border-box;
16 | box-shadow: none;
17 |
18 | @include mixins.border;
19 | }
20 |
21 | &--flat {
22 | box-shadow: none;
23 | }
24 |
25 | &--rounded {
26 | border-radius: 2rem;
27 | }
28 |
29 | &--fill {
30 | background-color: var(--surface-variant);
31 |
32 | &.q-card--primary {
33 | background-color: var(--primary-container);
34 | color: var(--on-primary-container);
35 | }
36 |
37 | &.q-card--secondary {
38 | background-color: var(--secondary-container);
39 | color: var(--on-secondary-container);
40 | }
41 |
42 | &.q-card--tertiary {
43 | background-color: var(--tertiary-container);
44 | color: var(--on-tertiary-container);
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/lib/components/drawer/QDrawer.scss:
--------------------------------------------------------------------------------
1 | @use "$css/mixins";
2 | @use "$css/variables";
3 |
4 | .q-drawer {
5 | z-index: 4;
6 | position: absolute;
7 | top: 0;
8 | right: auto;
9 | bottom: 0;
10 | left: auto;
11 |
12 | height: auto;
13 | @include mixins.padding("a-sm");
14 |
15 | background-color: var(--surface);
16 | color: var(--on-surface);
17 |
18 | overflow: auto;
19 |
20 | &.q-drawer--overlay {
21 | z-index: 6;
22 | }
23 |
24 | &__swipearea {
25 | position: absolute;
26 | top: 0;
27 | bottom: 0;
28 | z-index: 2;
29 |
30 | touch-action: none;
31 | }
32 |
33 | @each $side in ("left", "right") {
34 | &.q-drawer--#{$side} {
35 | #{$side}: 0;
36 | width: var(--#{$side}-drawer-width);
37 | transform: translate(if($side == "left", -100%, 100%));
38 | }
39 |
40 | &:not(.q-drawer--#{$side}).q-drawer--bordered {
41 | @include mixins.border(var(--outline), $side);
42 | }
43 | }
44 |
45 | &.q-drawer--active {
46 | transform: translate(0);
47 | }
48 | }
49 |
50 | @each $side in ("left", "right") {
51 | .q-drawer__swipearea--#{$side} {
52 | #{$side}: 0;
53 | width: 1.25rem;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/lib/components/separator/QSeparator.scss:
--------------------------------------------------------------------------------
1 | @use "$css/variables";
2 |
3 | .q-separator__wrapper {
4 | display: flex;
5 | align-items: center;
6 | justify-content: center;
7 | opacity: 0.75;
8 |
9 | &--inset {
10 | padding: 0 1rem;
11 | }
12 |
13 | &--vertical {
14 | height: 100%;
15 | flex-direction: column;
16 |
17 | &.q-separator__wrapper--inset {
18 | padding: 1rem 0;
19 | }
20 | }
21 |
22 | &:not(&--vertical) {
23 | width: 100%;
24 | }
25 | }
26 |
27 | .q-separator {
28 | border: none;
29 | flex: 1 1 auto;
30 |
31 | &--vertical {
32 | width: var(--q-separator--size, 0.0625rem);
33 |
34 | @each $space, $val in variables.$spaces {
35 | @if $space != "none" and $space != "auto" {
36 | &.q-separator__spacing--#{$space} {
37 | margin-inline: $val;
38 | }
39 | }
40 | }
41 | }
42 |
43 | &:not(&--vertical) {
44 | height: var(--q-separator--size, 0.0625rem);
45 |
46 | @each $space, $val in variables.$spaces {
47 | @if $space != "auto" {
48 | &.q-separator__spacing--#{$space} {
49 | margin-block: $val;
50 | }
51 | }
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import { sveltekit } from "@sveltejs/kit/vite";
3 | import { defineConfig } from "vitest/config";
4 | import versionPlugin from "./src/dev/versionPlugin";
5 | import docgenPlugin from "./src/dev/docgenPlugin";
6 |
7 | export default defineConfig({
8 | css: {
9 | preprocessorOptions: {
10 | scss: {
11 | api: "modern-compiler",
12 | },
13 | },
14 | },
15 | plugins: [versionPlugin(), docgenPlugin(), sveltekit()],
16 | test: {
17 | include: ["src/**/*.{test,spec}.{js,ts}"],
18 | },
19 | resolve: {
20 | alias: {
21 | $lib: path.resolve(__dirname, "./src/lib"),
22 | $components: path.resolve(__dirname, "./src/lib/components"),
23 | $composables: path.resolve(__dirname, "./src/lib/composables"),
24 | $utils: path.resolve(__dirname, "./src/lib/utils"),
25 | $css: path.resolve(__dirname, "./src/lib/css"),
26 | $stores: path.resolve(__dirname, "./src/lib/stores"),
27 | $helpers: path.resolve(__dirname, "./src/lib/helpers"),
28 | $docgen: path.resolve(__dirname, "./docgen"),
29 | $private: path.resolve(__dirname, "./src/lib/components/private"),
30 | },
31 | },
32 | });
33 |
--------------------------------------------------------------------------------
/src/lib/components/tabs/QTabs.scss:
--------------------------------------------------------------------------------
1 | @use "$css/mixins";
2 |
3 | .q-tabs {
4 | position: relative;
5 | display: flex;
6 | align-items: center;
7 | gap: 0;
8 | max-width: 100%;
9 | overflow: auto;
10 | scroll-behavior: smooth;
11 | border-radius: 0;
12 | border-bottom: solid 0.0625rem var(--outline);
13 |
14 | &::-webkit-scrollbar {
15 | display: none;
16 | }
17 |
18 | &.q-tabs--no-separator {
19 | border: unset;
20 | }
21 |
22 | &.q-tabs--secondary .q-tab .q-tab__indicator {
23 | border-radius: 0;
24 | height: 0.125rem;
25 | }
26 |
27 | &.q-tabs--vertical {
28 | flex-direction: column;
29 | width: fit-content;
30 | align-items: stretch;
31 | border-bottom: unset;
32 | border-right: solid 0.0625rem var(--outline);
33 |
34 | &.q-tabs--no-separator {
35 | border: unset;
36 | }
37 |
38 | & > .q-tab {
39 | @include mixins.padding("x-md");
40 |
41 | &:has(.q-tab__icon) {
42 | justify-content: flex-start;
43 | }
44 | }
45 |
46 | & .q-tab .q-tab__indicator {
47 | inset: 0 0 0 auto;
48 | border-radius: 0;
49 | height: unset;
50 | width: 0.125rem;
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/lib/components/select/index.scss:
--------------------------------------------------------------------------------
1 | @use "$css/mixins";
2 | @use "$css/variables";
3 | @use "$css/shared/q-field.scss";
4 |
5 | .q-select {
6 | &__option--selected {
7 | color: var(--primary);
8 | }
9 |
10 | .q-field__wrapper,
11 | .q-field__input {
12 | cursor: pointer;
13 | }
14 |
15 | &__menu {
16 | @include mixins.menu;
17 | opacity: 0;
18 | visibility: hidden;
19 | transform: scale(0.8) translateY(120%);
20 | transition:
21 | opacity variables.$speed2,
22 | transform variables.$speed2,
23 | visibility 0s variables.$speed2;
24 |
25 | &--active {
26 | opacity: 1;
27 | visibility: visible;
28 | transform: scale(1) translateY(100%);
29 | transition:
30 | opacity variables.$speed2,
31 | transform variables.$speed2,
32 | visibility 0s 0s;
33 | }
34 | }
35 |
36 | &.q-field--bottom-space &__menu {
37 | bottom: 1.0625rem;
38 | }
39 |
40 | & &__arrow-toggle {
41 | cursor: pointer;
42 |
43 | &--has-append {
44 | margin-left: 0.5rem;
45 | }
46 | }
47 |
48 | &.q-field--disable {
49 | opacity: 0.5;
50 | &,
51 | * {
52 | cursor: not-allowed;
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/.github/workflows/pages.yml:
--------------------------------------------------------------------------------
1 | name: Deploy Docs to GitHub Pages
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | jobs:
7 | build-and-deploy:
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - name: Checkout
12 | uses: actions/checkout@v5
13 |
14 | - name: Install Bun
15 | uses: oven-sh/setup-bun@v2
16 | with:
17 | bun-version: 1.3.1
18 |
19 | - name: Install Dependencies
20 | run: bun install --frozen-lockfile
21 |
22 | - name: Generate Plugins
23 | run: bun run gen:plugins
24 |
25 | - name: Build SvelteKit docs
26 | run: bun run build:docs
27 | env:
28 | BUILD_DOCS: "true"
29 |
30 | - name: Upload Artifacts
31 | uses: actions/upload-pages-artifact@v4
32 | with:
33 | path: "build/"
34 |
35 | deploy:
36 | needs: build-and-deploy
37 | runs-on: ubuntu-latest
38 |
39 | permissions:
40 | contents: read
41 | pages: write
42 | id-token: write
43 |
44 | environment:
45 | name: github-pages
46 | url: ${{ steps.deployment.outputs.page_url }}
47 |
48 | steps:
49 | - name: Deploy
50 | id: deployment
51 | uses: actions/deploy-pages@v4
52 |
--------------------------------------------------------------------------------
/src/lib/components/card/QCard.svelte:
--------------------------------------------------------------------------------
1 |
42 |
43 |
44 | {@render children?.()}
45 |
46 |
--------------------------------------------------------------------------------
/src/lib/components/railbar/QRailbar.scss:
--------------------------------------------------------------------------------
1 | @use "$css/mixins";
2 |
3 | .q-railbar {
4 | z-index: 5;
5 | position: fixed;
6 | top: 0;
7 | right: auto;
8 | bottom: 0;
9 | left: auto;
10 |
11 | display: flex;
12 | flex-direction: column;
13 | align-items: center;
14 | justify-content: flex-start;
15 | text-align: center;
16 | vertical-align: middle;
17 |
18 | height: auto;
19 | margin: 0;
20 | @include mixins.padding("a-sm");
21 | @include mixins.gap("md");
22 |
23 | background-color: var(--surface);
24 | color: var(--on-surface);
25 |
26 | border: 0;
27 | border-radius: 0;
28 |
29 | transform: none;
30 | white-space: nowrap;
31 | box-sizing: border-box;
32 | overflow: auto;
33 |
34 | // Hide the scrollbar
35 | // Internet Explorer 10+
36 | -ms-overflow-style: none;
37 | // Firefox
38 | scrollbar-width: none;
39 | // Other
40 | &::-webkit-scrollbar {
41 | display: none;
42 | }
43 |
44 | @each $side in ("left", "right") {
45 | &.q-railbar--#{$side} {
46 | #{$side}: 0;
47 | width: var(--#{$side}-railbar-width);
48 | }
49 |
50 | &:not(.q-railbar--#{$side}).q-railbar--bordered {
51 | @include mixins.border(var(--outline), $side);
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/lib/composables/useAlign.ts:
--------------------------------------------------------------------------------
1 | export type JustifyOptions =
2 | | "left"
3 | | "center"
4 | | "right"
5 | | "between"
6 | | "around"
7 | | "evenly"
8 | | "stretch";
9 |
10 | export type AlignOptions = "top" | "middle" | "bottom";
11 |
12 | export type UseAlignOptions =
13 | | `${AlignOptions}`
14 | | `${JustifyOptions}`
15 | | `${AlignOptions} ${JustifyOptions}`
16 | | `${JustifyOptions} ${AlignOptions}`;
17 |
18 | export interface UseAlignProps {
19 | align?: UseAlignOptions;
20 | }
21 |
22 | export const UseAlignPropsDefaults: UseAlignProps = {
23 | align: "top left",
24 | };
25 |
26 | const alignMap = {
27 | left: "start",
28 | center: "center",
29 | right: "end",
30 | between: "between",
31 | around: "around",
32 | evenly: "evenly",
33 | // @todo - justify-stretch isn't possible
34 | stretch: "stretch",
35 | } as const;
36 |
37 | export function useAlign(align: UseAlignOptions = "top left") {
38 | const alignments = align
39 | .split(" ")
40 | .map((entry) => {
41 | const val = alignMap[entry as keyof typeof alignMap];
42 | return val ? `justify-${val}` : false;
43 | })
44 | .filter((entry) => typeof entry === "string");
45 |
46 | return ["flex", ...alignments].join(" ");
47 | }
48 |
--------------------------------------------------------------------------------
/src/lib/components/card/props.ts:
--------------------------------------------------------------------------------
1 | import type { UseAlignProps } from "$composables";
2 | import type { HTMLAttributes } from "svelte/elements";
3 |
4 | export type QCardFillColors = "primary" | "secondary" | "tertiary";
5 |
6 | export interface QCardProps extends HTMLAttributes {
7 | /**
8 | * Adds a border around the card.
9 | * @default false
10 | */
11 | bordered?: boolean;
12 |
13 | /**
14 | * Defines the fill color of the card.
15 | * @default false
16 | */
17 | fill?: boolean | QCardFillColors;
18 |
19 | /**
20 | * Use the flat design for the card, removing its elevation.
21 | * @default false
22 | */
23 | flat?: boolean;
24 |
25 | /**
26 | * Adds border radius to the card to round its corners.
27 | * @default false
28 | */
29 | rounded?: boolean;
30 | }
31 |
32 | export interface QCardSectionProps extends HTMLAttributes {
33 | /**
34 | * Lays out the section content horizontally.
35 | * @default false
36 | */
37 | horizontal?: boolean;
38 | }
39 |
40 | export interface QCardActionsProps extends UseAlignProps, HTMLAttributes {
41 | /**
42 | * Lays out the action items vertically.
43 | * @default false
44 | */
45 | vertical?: boolean;
46 | }
47 |
--------------------------------------------------------------------------------
/src/lib/helpers/clickOutside.ts:
--------------------------------------------------------------------------------
1 | export function clickOutside(node: HTMLElement, onEventFunction: () => unknown) {
2 | const handleClick = (event: MouseEvent) => {
3 | const path = event.composedPath();
4 |
5 | if (!path.includes(node)) {
6 | onEventFunction();
7 | }
8 | };
9 |
10 | document.addEventListener("click", handleClick);
11 |
12 | return {
13 | destroy() {
14 | document.removeEventListener("click", handleClick);
15 | },
16 | };
17 | }
18 |
19 | export function clickOutsideDialog(node: HTMLDialogElement, onEventFunction: () => unknown) {
20 | const handleClick = (event: MouseEvent) => {
21 | if (!node.open) {
22 | return;
23 | }
24 | const rect = node.getBoundingClientRect();
25 |
26 | const isInDialog =
27 | rect.top <= event.clientY &&
28 | event.clientY <= rect.top + rect.height &&
29 | rect.left <= event.clientX &&
30 | event.clientX <= rect.left + rect.width;
31 |
32 | if (isInDialog === false && node.open === true) {
33 | onEventFunction();
34 | }
35 | };
36 |
37 | document.addEventListener("click", handleClick);
38 |
39 | return {
40 | destroy() {
41 | document.removeEventListener("click", handleClick);
42 | },
43 | };
44 | }
45 |
--------------------------------------------------------------------------------
/src/lib/components/card/docs.ts:
--------------------------------------------------------------------------------
1 | import type { QComponentDocs } from "$utils";
2 | import {
3 | QCardActionsDocsProps,
4 | QCardActionsDocsSnippets,
5 | QCardDocsProps,
6 | QCardDocsSnippets,
7 | QCardSectionDocsProps,
8 | QCardSectionDocsSnippets,
9 | } from "./docs.props";
10 |
11 | export const QCardDocs: QComponentDocs = {
12 | name: "QCard",
13 | description:
14 | "Cards provide a clean, flexible, and convenient means of displaying a wide variety of content.",
15 | docs: {
16 | props: QCardDocsProps,
17 | snippets: QCardDocsSnippets,
18 | methods: [],
19 | events: [],
20 | },
21 | };
22 |
23 | export const QCardSectionDocs: QComponentDocs = {
24 | name: "QCardSection",
25 | description: "Sections are used to group similar content within a card.",
26 | docs: {
27 | props: QCardSectionDocsProps,
28 | snippets: QCardSectionDocsSnippets,
29 | methods: [],
30 | events: [],
31 | },
32 | };
33 |
34 | export const QCardActionsDocs: QComponentDocs = {
35 | name: "QCardActions",
36 | description: "Actions hold actionable items like buttons within a card.",
37 | docs: {
38 | props: QCardActionsDocsProps,
39 | snippets: QCardActionsDocsSnippets,
40 | methods: [],
41 | events: [],
42 | },
43 | };
44 |
--------------------------------------------------------------------------------
/src/lib/components/list/QItemSection.scss:
--------------------------------------------------------------------------------
1 | @use "$css/mixins";
2 |
3 | .q-item__section {
4 | display: flex;
5 |
6 | @include mixins.typography("body-medium");
7 | color: var(--on-surface-variant);
8 |
9 | &--headline {
10 | max-width: 100%;
11 | @include mixins.typography("body-large");
12 | color: var(--on-surface);
13 | }
14 |
15 | &--trailing {
16 | @include mixins.typography("label-small");
17 | }
18 |
19 | &--video {
20 | margin-left: -1rem;
21 | padding-block: 0.25rem;
22 |
23 | & video {
24 | width: auto;
25 | height: 4rem;
26 | object-fit: cover;
27 | object-position: center;
28 | }
29 | }
30 |
31 | & .q-avatar {
32 | height: 2.5rem;
33 | width: 2.5rem;
34 | }
35 | &--icon {
36 | height: 1.5rem;
37 | width: 1.5rem;
38 | }
39 |
40 | &--thumbnail img {
41 | width: 3.5rem;
42 | height: 3.5rem;
43 | object-fit: cover;
44 | object-position: center;
45 | }
46 |
47 | &--content {
48 | flex-direction: column;
49 | flex: 10000 1 0;
50 | justify-content: center;
51 | align-items: start;
52 | }
53 |
54 | &--toggle {
55 | align-items: center;
56 | justify-content: center;
57 | height: 1.5rem;
58 | width: 3.25rem;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/lib/css/mixins/_spaces.scss:
--------------------------------------------------------------------------------
1 | @use "sass:map";
2 | @use "sass:string";
3 | @use "../variables";
4 |
5 | @mixin space($type: null, $spaces...) {
6 | @each $space in $spaces {
7 | $pos: string.slice($space, 1, 1);
8 | @if not map.has-key(variables.$positions, $pos) {
9 | @error "Position error for #{$type}, \"#{$pos}\" is not a valid position. Should be one of a, x, y, t, r, b or l";
10 | }
11 | $pos: map.get(variables.$positions, $pos);
12 |
13 | $spacing: string.slice($space, 3);
14 | @if not map.has-key(variables.$spaces, $spacing) {
15 | @error "Spacing error for #{$type}, \"#{$space}\" is not a valid spacing. Should be one of auto, none, xs, sm, md, lg or xl";
16 | }
17 | $spacing: map.get(variables.$spaces, $spacing);
18 |
19 | #{$type}#{$pos}: $spacing;
20 | }
21 | }
22 |
23 | @mixin padding($paddings...) {
24 | @include space("padding", $paddings...);
25 | }
26 |
27 | @mixin margin($margins...) {
28 | @include space("margin", $margins...);
29 | }
30 |
31 | @mixin gap($space: "sm") {
32 | @if not map.has-key(variables.$spaces, $space) {
33 | @error "Spacing error for gap, \"#{$space}\" is not a valid spacing. Should be one of auto, none, xs, sm, md, lg or xl";
34 | }
35 | gap: map.get(variables.$spaces, $space);
36 | }
37 |
--------------------------------------------------------------------------------
/plugins/class-preprocessor/types.ts:
--------------------------------------------------------------------------------
1 | import { Node as EstreeNode, NodeMap } from "estree";
2 | import type { AST } from "svelte/compiler";
3 |
4 | export type Script = NonNullable;
5 | export type Fragment = AST.Root["fragment"];
6 |
7 | export interface ClassesDefinition {
8 | start: number;
9 | end: number;
10 | classes: string[];
11 | }
12 |
13 | export interface ClassesUsage {
14 | /*
15 | * v here
16 | * Start of the class attribute value .
17 | */
18 | start: number;
19 | /*
20 | * v here
21 | * End of the class attribute value
.
22 | */
23 | end: number;
24 | }
25 |
26 | export interface Result {
27 | def: ClassesDefinition;
28 | uses: ClassesUsage[];
29 | }
30 |
31 | export type ComponentName = `q-${string}`;
32 |
33 | export type ScriptDef = Record
;
34 |
35 | export type ClassAttribute = EstreeNode & { type: "Attribute"; name: "class" };
36 |
37 | export type Node = T extends undefined
38 | ? AST.BaseNode & EstreeNode
39 | : T extends keyof NodeMap
40 | ? AST.BaseNode & NodeMap[T]
41 | : AST.BaseNode & EstreeNode & { type: T };
42 |
--------------------------------------------------------------------------------
/src/lib/components/list/docs.ts:
--------------------------------------------------------------------------------
1 | import type { QComponentDocs } from "$utils";
2 | import {
3 | QItemDocsProps,
4 | QItemDocsSnippets,
5 | QItemSectionDocsProps,
6 | QItemSectionDocsSnippets,
7 | QListDocsProps,
8 | QListDocsSnippets,
9 | } from "./docs.props";
10 |
11 | export const QListDocs: QComponentDocs = {
12 | name: "QList",
13 | description:
14 | "The QList component is used to display a list of items with options for adding text, icons and actions.",
15 | docs: {
16 | props: QListDocsProps,
17 | snippets: QListDocsSnippets,
18 | methods: [],
19 | events: [],
20 | },
21 | };
22 |
23 | export const QItemDocs: QComponentDocs = {
24 | name: "QItem",
25 | description:
26 | "The QItem component is generally used inside lists to display related pieces of information.",
27 | docs: {
28 | props: QItemDocsProps,
29 | snippets: QItemDocsSnippets,
30 | methods: [],
31 | events: [],
32 | },
33 | };
34 |
35 | export const QItemSectionDocs: QComponentDocs = {
36 | name: "QItemSection",
37 | description:
38 | "The QItemSection component is used inside QItem to separate different types of information.",
39 | docs: {
40 | props: QItemSectionDocsProps,
41 | snippets: QItemSectionDocsSnippets,
42 | methods: [],
43 | events: [],
44 | },
45 | };
46 |
--------------------------------------------------------------------------------
/src/lib/composables/useRouterLink.ts:
--------------------------------------------------------------------------------
1 | import { LayoutParams, RouteId } from "$app/types";
2 | import { createClasses } from "$utils";
3 | import type { Page } from "@sveltejs/kit";
4 |
5 | export interface UseRouterLinkProps {
6 | href?: string;
7 | to?: string;
8 | disable?: boolean;
9 | activeClass?: string;
10 | replace?: boolean;
11 | }
12 |
13 | export const UseRouterLinkPropsDefaults: UseRouterLinkProps = {
14 | href: undefined,
15 | to: undefined,
16 | disable: false,
17 | activeClass: undefined,
18 | replace: false,
19 | };
20 |
21 | export function isRouteActive(
22 | router: Page, RouteId | null>,
23 | to: string | undefined
24 | ) {
25 | return to === "/"
26 | ? router.url.pathname === to
27 | : router.url.pathname.slice(0, (to || "").length) === to;
28 | }
29 |
30 | export function useRouterLink(props: T) {
31 | const hasLink = [props.to, props.href].some((entry) => typeof entry !== "undefined");
32 | const linkClasses = createClasses([hasLink && "q-link", props.disable && "disable"]);
33 |
34 | const linkAttributes = {
35 | href: props.to || props.href,
36 | "data-sveltkit-reload": props.replace ? "" : undefined,
37 | };
38 |
39 | return {
40 | hasLink,
41 | linkAttributes,
42 | linkClasses,
43 | };
44 | }
45 |
--------------------------------------------------------------------------------
/src/lib/components/avatar/QAvatar.svelte:
--------------------------------------------------------------------------------
1 |
28 |
29 |
30 | {#if video}
31 |
32 | {#if sources && sources.length > 0}
33 | {#each sources as { src, type } (type)}
34 |
35 | {/each}
36 | {:else if src}
37 |
38 | {/if}
39 |
40 | {@render videoAccessibility?.()}
41 |
42 | {:else if src}
43 |
44 | {:else}
45 | {@render children?.()}
46 | {/if}
47 |
48 |
--------------------------------------------------------------------------------
/docgen/types/parseTypes.ts:
--------------------------------------------------------------------------------
1 | import ts from "typescript";
2 |
3 | export default async function parseTypes(fileName: string) {
4 | const program = ts.createProgram([fileName], { allowJs: true });
5 | program.getTypeChecker();
6 | const types: Record = {};
7 |
8 | const visit = (node: ts.Node) => {
9 | if (!isNodeExported(node)) {
10 | return;
11 | }
12 |
13 | if (ts.isTypeAliasDeclaration(node) || ts.isInterfaceDeclaration(node)) {
14 | const name = node.name.text;
15 | if (!name.includes("Props")) {
16 | const declaration = node
17 | .getText()
18 | .replace("; ", ", ")
19 | .replace(";", "")
20 | .replace("export ", "")
21 | .trim();
22 |
23 | types[name] = declaration;
24 | }
25 | }
26 |
27 | ts.forEachChild(node, visit);
28 | };
29 |
30 | for (const sourceFile of program.getSourceFiles()) {
31 | if (!sourceFile.isDeclarationFile) {
32 | ts.forEachChild(sourceFile, visit);
33 | }
34 | }
35 |
36 | function isNodeExported(node: ts.Node): boolean {
37 | return (
38 | (ts.getCombinedModifierFlags(node as ts.Declaration) & ts.ModifierFlags.Export) !== 0 ||
39 | (!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile)
40 | );
41 | }
42 |
43 | return types;
44 | }
45 |
--------------------------------------------------------------------------------
/src/lib/utils/types.ts:
--------------------------------------------------------------------------------
1 | import { ParsedProp, ParsedSnippet } from "../../../docgen/props/parseInterface";
2 |
3 | export interface NativeProps {
4 | userClasses?: string | null;
5 | userStyles?: string | null;
6 | }
7 |
8 | export const NativePropsDefaults: NativeProps = {
9 | userClasses: undefined,
10 | userStyles: undefined,
11 | };
12 |
13 | export type QuaffSizes = "none" | "xs" | "sm" | "md" | "lg" | "xl";
14 |
15 | export type CssUnit = "px" | "%" | "em" | "ex" | "ch" | "rem" | "vw" | "vh" | "vmin" | "vmax";
16 |
17 | export type CssValue = `${number}${CssUnit}`;
18 |
19 | export interface QComponentDocs {
20 | name: string;
21 | description: string;
22 | docs: {
23 | props: ParsedProp[];
24 | snippets: ParsedSnippet[];
25 | methods: QComponentMethod[];
26 | events: QComponentEvent[];
27 | };
28 | }
29 |
30 | export interface QComponentType {
31 | name: string;
32 | description: string;
33 | }
34 |
35 | export interface QComponentEvent {
36 | name: string;
37 | type: string;
38 | description: string;
39 | }
40 |
41 | export interface QComponentMethod {
42 | name: string;
43 | type: string;
44 | description: string;
45 | }
46 |
47 | export type Entries = {
48 | [K in keyof T]: [K, T[K]];
49 | }[keyof T][];
50 |
51 | export type QEvent = T & {
52 | currentTarget: EventTarget & E;
53 | };
54 |
--------------------------------------------------------------------------------
/src/lib/components/icon/QIcon.svelte:
--------------------------------------------------------------------------------
1 |
40 |
41 |
42 | {#if name !== undefined}
43 | {name}
44 | {:else if img !== undefined}
45 |
46 | {:else if svg}
47 | {@html svg}
48 | {:else}
49 | {@render children?.()}
50 | {/if}
51 |
52 |
--------------------------------------------------------------------------------
/docgen/snippets/getInfo.ts:
--------------------------------------------------------------------------------
1 | import { readFile } from "fs/promises";
2 | import pathExists from "../helpers/pathExists.js";
3 | import generateHash from "../helpers/generateHash.js";
4 | import extractHash from "../helpers/extractHash.js";
5 | import parseSvelteFile from "./parseSvelteFile.js";
6 | import type { SnippetSection } from "./parseSvelteFile.js";
7 |
8 | type InfoObject = {
9 | needsToBeGenerated: boolean;
10 | hashSections?: string;
11 | sections?: SnippetSection[];
12 | };
13 |
14 | function stringifySectionsForHash(sections: Record[]) {
15 | return JSON.stringify(sections);
16 | }
17 |
18 | export default async function getInfo(
19 | pageFilePath: string,
20 | docsSnippetsFilePath: string
21 | ): Promise {
22 | if (!(await pathExists(pageFilePath))) {
23 | return { needsToBeGenerated: false };
24 | }
25 |
26 | const sections = await parseSvelteFile(pageFilePath);
27 | const hashSections = generateHash(stringifySectionsForHash(sections));
28 |
29 | if (!(await pathExists(docsSnippetsFilePath))) {
30 | return { needsToBeGenerated: true, hashSections, sections };
31 | }
32 |
33 | const hashFromDocsProps = extractHash(await readFile(docsSnippetsFilePath, "utf8"));
34 | const needsToBeGenerated = hashSections !== hashFromDocsProps;
35 |
36 | return { needsToBeGenerated, hashSections, sections };
37 | }
38 |
--------------------------------------------------------------------------------
/src/lib/components/progress/QCircularProgress.scss:
--------------------------------------------------------------------------------
1 | @use "$css/mixins";
2 |
3 | .q-circular-progress {
4 | display: inline-block;
5 | position: relative;
6 | vertical-align: middle;
7 |
8 | height: 1em;
9 | width: 1em;
10 | font-size: var(--size);
11 | line-height: 0;
12 |
13 | &__svg {
14 | height: 100%;
15 | width: 100%;
16 | }
17 |
18 | &--indeterminate {
19 | & .q-circular-progress__svg {
20 | transform-origin: 50% 50%;
21 | animation: q-spin 2s linear infinite;
22 |
23 | & .q-circular-progress__indicator {
24 | stroke-dasharray: 1400;
25 | stroke-dashoffset: 0;
26 | animation: q-load 1.5s ease-in-out infinite;
27 | }
28 | }
29 | }
30 | }
31 |
32 | @keyframes q-spin {
33 | 0% {
34 | transform: rotate3d(0, 0, 1, 0deg);
35 | }
36 | 25% {
37 | transform: rotate3d(0, 0, 1, 90deg);
38 | }
39 | 50% {
40 | transform: rotate3d(0, 0, 1, 180deg);
41 | }
42 | 75% {
43 | transform: rotate3d(0, 0, 1, 270deg);
44 | }
45 | 100% {
46 | transform: rotate3d(0, 0, 1, 359deg);
47 | }
48 | }
49 |
50 | @keyframes q-load {
51 | 0% {
52 | stroke-dasharray: 1, 400;
53 | stroke-dashoffset: 0;
54 | }
55 | 50% {
56 | stroke-dasharray: 400, 400;
57 | stroke-dashoffset: -100;
58 | }
59 | 100% {
60 | stroke-dasharray: 400, 400;
61 | stroke-dashoffset: -300;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/lib/components/tabs/props.ts:
--------------------------------------------------------------------------------
1 | import type { MaterialSymbol } from "material-symbols";
2 | import type { Snippet } from "svelte";
3 | import type { HTMLAttributes } from "svelte/elements";
4 |
5 | export type QTabsVariants = "primary" | "secondary" | "vertical";
6 |
7 | export interface QTabsProps extends HTMLAttributes {
8 | /**
9 | * Current active tab name. This property is bindable.
10 | *
11 | * @default undefined
12 | */
13 | value?: string;
14 |
15 | /**
16 | * Visual style variant of the tabs.
17 | *
18 | * @default "primary"
19 | */
20 | variant?: QTabsVariants;
21 |
22 | /**
23 | * Removes the separator line under the tabs (or to the right for vertical tabs).
24 | *
25 | * @default false
26 | */
27 | noSeparator?: boolean;
28 | }
29 |
30 | export interface QTabProps extends HTMLAttributes {
31 | /**
32 | * Unique identifier for the tab. Used to match with QTabs' value prop.
33 | */
34 | name: string;
35 |
36 | /**
37 | * Navigation target URL when the tab is used as a router link.
38 | * When provided, the tab will render as an anchor tag.
39 | *
40 | * @default undefined
41 | */
42 | to?: string;
43 |
44 | /**
45 | * Icon to display in the tab. Can be a Material Symbol name or a custom snippet.
46 | *
47 | * @default undefined
48 | */
49 | icon?: MaterialSymbol | Snippet;
50 | }
51 |
--------------------------------------------------------------------------------
/src/dev/colorgenPlugin.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs";
2 | import { format } from "prettier";
3 | import { generateColors } from "../lib/utils/colors.js";
4 | import type { QuaffColors, Mode } from "../lib/utils/colors.js";
5 | import type { Entries } from "../lib/utils/types.js";
6 |
7 | const BASE_COLOR = "#0039b4";
8 |
9 | function stringFromPalette(palette: QuaffColors, mode: Mode, type: "root" | "body") {
10 | const modeString = mode ? `-${mode}` : "";
11 | return Object.entries(palette)
12 | .map(([color, hex]) =>
13 | type === "root" ? `--${color}${modeString}: ${hex};` : `--${color}: var(--${color}-${mode});`
14 | )
15 | .join("");
16 | }
17 |
18 | export function writeColorFile() {
19 | const colors = generateColors(BASE_COLOR);
20 | const colorEntries = Object.entries(colors) as Entries;
21 |
22 | const disclaimer = "// AUTO GENERATED FILE - DO NOT MODIFY OR DELETE";
23 | const root = `:root {${colorEntries
24 | .map(([mode, palette]) => stringFromPalette(palette, mode, "root"))
25 | .join("")}}`;
26 | const cssContent = colorEntries
27 | .map(([mode, palette]) => `.body--${mode} {${stringFromPalette(palette, mode, "body")}}`)
28 | .join("");
29 |
30 | const content = [disclaimer, root, cssContent].join("\n\n");
31 |
32 | const pathToCssFile = new URL("../lib/css/theme/_colors.scss", import.meta.url);
33 |
34 | format(content, {
35 | parser: "css",
36 | }).then((content) => fs.writeFileSync(pathToCssFile, content));
37 | }
38 |
--------------------------------------------------------------------------------
/src/lib/components/icon/props.ts:
--------------------------------------------------------------------------------
1 | import type { HTMLAttributes, HTMLImgAttributes } from "svelte/elements";
2 | import type { MaterialSymbol } from "material-symbols";
3 |
4 | export type QIconSizeOptions = Q.Size | Q.CssValue | number;
5 |
6 | export type QIconTypeOptions = "outlined" | "sharp" | "rounded";
7 |
8 | export interface QIconProps extends HTMLAttributes {
9 | /**
10 | * The size of the icon. Can be specified with CSS units. If no unit is specified, "px" will be used.
11 | * @default md
12 | */
13 | size?: QIconSizeOptions;
14 |
15 | /**
16 | * The type of the icon.
17 | * @default outlined
18 | */
19 | type?: QIconTypeOptions;
20 |
21 | /**
22 | * The name of the Material Symbols icon.
23 | * @default undefined
24 | */
25 | name?: MaterialSymbol | `img:${string}`;
26 |
27 | /**
28 | * Determines whether the icon should be filled.
29 | * @default false
30 | */
31 | filled?: boolean;
32 |
33 | /**
34 | * The SVG content for the icon.
35 | * @default undefined
36 | */
37 | svg?: string;
38 |
39 | /**
40 | * The image source for the icon.
41 | * @default undefined
42 | */
43 | img?: string;
44 |
45 | /**
46 | * Additional attributes for the image element when using the `img` prop, as for example the "alt" attribute.
47 | * @default {}
48 | */
49 | imgAttributes?: HTMLImgAttributes;
50 |
51 | /**
52 | * The color of the icon.
53 | * @default undefined
54 | */
55 | color?: string;
56 | }
57 |
--------------------------------------------------------------------------------
/src/lib/css/theme/_elevate.scss:
--------------------------------------------------------------------------------
1 | // https://m3.material.io/styles/elevation/tokens
2 |
3 | :root {
4 | --elevate0: 0 0 0 0 rgb(0 0 0 / 0);
5 | --elevate1: 0 0.0625rem 0.0625rem 0 rgb(0 0 0 / 0.2); /* 1dp */
6 | --elevate2: 0 0.125rem 0.125rem 0 rgb(0 0 0 / 0.32); /* 3dp */
7 | --elevate3: 0 0.25rem 0.25rem 0 rgb(0 0 0 / 0.4); /* 6dp */
8 | --elevate4: 0 0.375rem 0.375rem 0 rgb(0 0 0 / 0.48); /* 8dp */
9 | --elevate5: 0 0.5rem 0.5rem 0 rgb(0 0 0 / 0.56); /* 12dp */
10 | }
11 |
12 | .elevate-0 {
13 | box-shadow: var(--elevate0);
14 | }
15 |
16 | .elevate-1 {
17 | box-shadow: var(--elevate1);
18 | }
19 |
20 | .elevate-2 {
21 | box-shadow: var(--elevate2);
22 | }
23 |
24 | .elevate-3 {
25 | box-shadow: var(--elevate3);
26 | }
27 |
28 | .elevate-4 {
29 | box-shadow: var(--elevate4);
30 | }
31 |
32 | .elevate-5 {
33 | box-shadow: var(--elevate5);
34 | }
35 |
36 | // For API compatibility. We might make this deprecated in the future
37 | @for $i from 1 through 24 {
38 | $elevation: null;
39 |
40 | @if $i <= 3 {
41 | $elevation: var(--elevate0);
42 | } @else if $i > 3 and $i <= 7 {
43 | $elevation: var(--elevate1);
44 | } @else if $i > 7 and $i <= 12 {
45 | $elevation: var(--elevate2);
46 | } @else if $i > 12 and $i <= 18 {
47 | $elevation: var(--elevate3);
48 | } @else if $i > 18 and $i <= 21 {
49 | $elevation: var(--elevate4);
50 | } @else if $i > 21 {
51 | $elevation: var(--elevate5);
52 | }
53 |
54 | .shadow-#{$i} {
55 | box-shadow: $elevation;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/lib/css/_components.scss:
--------------------------------------------------------------------------------
1 | @forward "$components/avatar/QAvatar.scss";
2 | @forward "$components/breadcrumbs/QBreadcrumbs.scss";
3 | @forward "$components/breadcrumbs/QBreadcrumbsEl.scss";
4 | @forward "$components/button/QBtn.scss";
5 | @forward "$components/card/QCard.scss";
6 | @forward "$components/card/QCardActions.scss";
7 | @forward "$components/card/QCardSection.scss";
8 | @forward "$components/checkbox";
9 | @forward "$components/chip/QChip.scss";
10 | @forward "$components/dialog/QDialog.scss";
11 | @forward "$components/drawer/QDrawer.scss";
12 | @forward "$components/expansion-item/QExpansionItem.scss";
13 | @forward "$components/footer/QFooter.scss";
14 | @forward "$components/header/QHeader.scss";
15 | @forward "$components/icon/QIcon.scss";
16 | @forward "$components/input";
17 | @forward "$components/layout/QLayout.scss";
18 | @forward "$components/list/QItem.scss";
19 | @forward "$components/list/QItemSection.scss";
20 | @forward "$components/list/QList.scss";
21 | @forward "$components/progress/QCircularProgress.scss";
22 | @forward "$components/progress/QLinearProgress.scss";
23 | @forward "$components/radio";
24 | @forward "$components/railbar/QRailbar.scss";
25 | @forward "$components/select";
26 | @forward "$components/separator/QSeparator.scss";
27 | @forward "$components/switch/QSwitch.scss";
28 | @forward "$components/table";
29 | @forward "$components/tabs/QTab.scss";
30 | @forward "$components/tabs/QTabs.scss";
31 | @forward "$components/toolbar/QToolbar.scss";
32 | @forward "$components/tooltip/QTooltip.scss";
33 |
--------------------------------------------------------------------------------
/src/lib/components/table/index.scss:
--------------------------------------------------------------------------------
1 | @use "$css/mixins";
2 |
3 | .q-table {
4 | &__table {
5 | @include mixins.table;
6 |
7 | border-radius: 0.75rem;
8 | box-shadow: var(--elevate1);
9 |
10 | th {
11 | font-weight: 600;
12 |
13 | .q-icon {
14 | position: relative;
15 | top: -1px;
16 | opacity: 0;
17 |
18 | &.q-icon--sort {
19 | opacity: 1;
20 | }
21 | }
22 |
23 | &:hover {
24 | .q-icon:not(.q-icon--sort) {
25 | opacity: 0.5;
26 | }
27 | .q-icon--sort {
28 | opacity: 0.8;
29 | }
30 | }
31 | }
32 |
33 | tr:last-child td {
34 | border-bottom: none;
35 | }
36 |
37 | &--flat {
38 | box-shadow: none;
39 | }
40 |
41 | &--bordered {
42 | border: 1px solid var(--outline);
43 |
44 | tr:last-child td {
45 | border-bottom: none;
46 | }
47 | }
48 |
49 | &--dense {
50 | border-radius: 0.5rem;
51 |
52 | th,
53 | td {
54 | padding: 0.25rem;
55 | }
56 | }
57 |
58 | > * {
59 | border-radius: 0;
60 | }
61 | }
62 |
63 | &__footer {
64 | display: flex;
65 | justify-content: end;
66 | align-items: center;
67 |
68 | &-records-per-page-select {
69 | margin-bottom: 0 !important;
70 | margin-left: 1em;
71 | margin-right: 1em;
72 | }
73 |
74 | &-records-per-page-select {
75 | width: 80px;
76 | display: inline-block;
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/lib/components/tooltip/props.ts:
--------------------------------------------------------------------------------
1 | import type { HTMLAttributes } from "svelte/elements";
2 |
3 | export type QTooltipPosition =
4 | | "top-left"
5 | | "top"
6 | | "top-right"
7 | | "right"
8 | | "bottom-right"
9 | | "bottom"
10 | | "bottom-left"
11 | | "left";
12 |
13 | export type QTooltipOffset = { x?: number; y?: number };
14 |
15 | export interface QTooltipProps extends HTMLAttributes {
16 | /**
17 | * The target element the tooltip should be attached to. Can be an HTML element or a CSS selector. If not specified, the tooltip will be attached to the nearest Quaff component in the parent tree.
18 | * @default undefined
19 | */
20 | target?: T;
21 |
22 | /**
23 | * Defines the show/hide state of the tooltip. By default, the tooltip will be be shown on mouseenter and hidden on mouseleave.
24 | * @default false
25 | */
26 | value?: boolean;
27 |
28 | /**
29 | * Defines the position of the tooltip.
30 | * @default "bottom"
31 | */
32 | position?: QTooltipPosition;
33 |
34 | /**
35 | * Offset of the tooltip in pixels. Positive values move the tooltip down/right, negative values move the tooltip up/left.
36 | * @default { x: 0, y: 0 }
37 | */
38 | offset?: QTooltipOffset;
39 |
40 | /**
41 | * Delay in milliseconds before the tooltip appears.
42 | * @default 0
43 | */
44 | delay?: number;
45 |
46 | /**
47 | * Delay in milliseconds before the tooltip is hidden.
48 | * @default 0
49 | */
50 | hideDelay?: number;
51 | }
52 |
--------------------------------------------------------------------------------
/src/lib/css/mixins/_responsive.scss:
--------------------------------------------------------------------------------
1 | @use "sass:map";
2 | @use "$css/variables";
3 |
4 | @mixin xs {
5 | @media only screen and (max-width: #{map.get(variables.$breakpoints, sm) - 1px}) {
6 | @content;
7 | }
8 | }
9 |
10 | @mixin sm {
11 | @media only screen and (min-width: #{map.get(variables.$breakpoints, sm)}) and (max-width: #{map.get(variables.$breakpoints, md) - 1px}) {
12 | @content;
13 | }
14 | }
15 |
16 | @mixin md {
17 | @media only screen and (min-width: #{map.get(variables.$breakpoints, md)}) and (max-width: #{map.get(variables.$breakpoints, lg) - 1px}) {
18 | @content;
19 | }
20 | }
21 |
22 | @mixin lg {
23 | @media only screen and (min-width: #{map.get(variables.$breakpoints, lg)}) and (max-width: #{map.get(variables.$breakpoints, xl) - 1px}) {
24 | @content;
25 | }
26 | }
27 |
28 | @mixin xl {
29 | @media only screen and (min-width: #{map.get(variables.$breakpoints, xl)}) {
30 | @content;
31 | }
32 | }
33 |
34 | @mixin up-to-sm {
35 | @media only screen and (max-width: #{map.get(variables.$breakpoints, sm) - 1px}) {
36 | @content;
37 | }
38 | }
39 |
40 | @mixin up-to-md {
41 | @media only screen and (max-width: #{map.get(variables.$breakpoints, md) - 1px}) {
42 | @content;
43 | }
44 | }
45 |
46 | @mixin up-to-lg {
47 | @media only screen and (max-width: #{map.get(variables.$breakpoints, lg) - 1px}) {
48 | @content;
49 | }
50 | }
51 |
52 | @mixin up-to-xl {
53 | @media only screen and (max-width: #{map.get(variables.$breakpoints, xl) - 1px}) {
54 | @content;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/lib/css/mixins/_image.scss:
--------------------------------------------------------------------------------
1 | @use "$css/variables";
2 | @use "spaces";
3 |
4 | @mixin responsive($parent: null) {
5 | object-fit: cover;
6 | object-position: center;
7 | transition:
8 | variables.$speed3 transform,
9 | variables.$speed3 border-radius,
10 | variables.$speed3 padding;
11 | width: 100%;
12 | height: 100%;
13 |
14 | @include spaces.margin("x-auto");
15 |
16 | @if $parent != null {
17 | width: 2.5rem;
18 | border: 0.25rem solid transparent;
19 | }
20 | }
21 |
22 | @mixin btn-image($parent: "btn") {
23 | &.q-#{$parent}--sm > .q-#{$parent}__img--responsive {
24 | height: 2rem;
25 | width: 2rem;
26 | }
27 |
28 | &.q-#{$parent}--lg > .q-#{$parent}__img--responsive {
29 | height: 3rem;
30 | width: 3rem;
31 | }
32 |
33 | &.q-#{$parent}--xl > .q-#{$parent}__img--responsive {
34 | height: 3.5rem;
35 | width: 3.5rem;
36 | }
37 |
38 | & > .q-#{$parent}__img {
39 | margin-inline: -0.5rem;
40 | }
41 |
42 | & > .q-#{$parent}__img:not(.q-#{$parent}__img--responsive) {
43 | min-width: 1.5rem;
44 | max-width: 1.5rem;
45 | min-height: 1.5rem;
46 | max-height: 1.5rem;
47 | }
48 |
49 | & > .q-#{$parent}__img.q-#{$parent}__img--responsive {
50 | @include responsive($parent);
51 |
52 | $marginSize: -1.5rem;
53 | @if $parent == "chip" {
54 | $marginSize: -1rem;
55 | }
56 |
57 | margin-left: $marginSize !important;
58 |
59 | &.q-#{$parent}__img--trailing {
60 | margin-left: -0.5rem;
61 | margin-right: $marginSize;
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/lib/components/switch/props.ts:
--------------------------------------------------------------------------------
1 | import type { MaterialSymbol } from "material-symbols";
2 | import type { Snippet } from "svelte";
3 | import type { HTMLAttributes } from "svelte/elements";
4 |
5 | export interface QSwitchProps extends HTMLAttributes {
6 | /**
7 | * Current on/off state of the switch. This property is bindable.
8 | *
9 | * @default undefined
10 | */
11 | value?: boolean;
12 |
13 | /**
14 | * Text label to display next to the switch.
15 | *
16 | * @default undefined
17 | */
18 | label?: string;
19 |
20 | /**
21 | * Position of the label relative to the switch.
22 | *
23 | * @default "right"
24 | */
25 | labelPosition?: "left" | "right";
26 |
27 | /**
28 | * Shows default check/close icons in the switch handle.
29 | *
30 | * @default false
31 | */
32 | icons?: boolean;
33 |
34 | /**
35 | * When true, only shows the check icon (when the switch is on).
36 | *
37 | * @default false
38 | */
39 | showOnlyCheckedIcon?: boolean;
40 |
41 | /**
42 | * Custom icon to show when the switch is on. Can be a Material Symbol name or a custom snippet.
43 | *
44 | * @default "check"
45 | */
46 | checkedIcon?: MaterialSymbol | Snippet;
47 |
48 | /**
49 | * Custom icon to show when the switch is off. Can be a Material Symbol name or a custom snippet.
50 | *
51 | * @default "close"
52 | */
53 | uncheckedIcon?: MaterialSymbol | Snippet;
54 |
55 | /**
56 | * Disables the switch, preventing user interaction.
57 | *
58 | * @default false
59 | */
60 | disabled?: boolean;
61 | }
62 |
--------------------------------------------------------------------------------
/src/lib/components/list/QItem.scss:
--------------------------------------------------------------------------------
1 | @use "$css/mixins";
2 |
3 | .q-item {
4 | display: flex;
5 | align-items: center;
6 | justify-content: flex-start;
7 | white-space: nowrap;
8 | gap: 1rem;
9 |
10 | min-width: 100%;
11 | max-width: 100%;
12 | min-height: 3.5rem;
13 | margin-top: 0 !important;
14 | margin: 0;
15 | padding: 0.5rem 1.5rem 0.5rem 1rem;
16 | gap: 1rem;
17 |
18 | &--clickable {
19 | cursor: pointer;
20 | }
21 |
22 | &--dense {
23 | min-height: 2rem;
24 | }
25 |
26 | &:is(.q-link, &--clickable:not(label)) {
27 | padding: 0.5rem 1.5rem 0.5rem 1rem;
28 |
29 | &:is(:hover, :focus):not([aria-disabled])::after {
30 | content: "";
31 | position: absolute;
32 | top: 0;
33 | left: 0;
34 | width: 100%;
35 | height: 100%;
36 | background-color: var(--on-surface);
37 | border-radius: inherit;
38 | }
39 |
40 | &:hover:not([aria-disabled])::after {
41 | opacity: 0.08;
42 | }
43 |
44 | &:focus:not([aria-disabled])::after {
45 | opacity: 0.16;
46 | }
47 |
48 | &.multiline {
49 | padding-block: 0.75rem;
50 | }
51 |
52 | &::after {
53 | background-image: radial-gradient(circle, rgb(150 150 150 / 0.2) 1%, transparent 1%);
54 | }
55 | }
56 |
57 | &--multiline {
58 | padding-block: 0.75rem;
59 |
60 | & > .q-item__section:is(.avatar, .icon, .thumbnail, .trailingIcon, .trailingText) {
61 | align-self: start;
62 | }
63 |
64 | > .q-item__section.video {
65 | margin-left: -1rem !important;
66 | padding-block: 0;
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/lib/components/table/props.ts:
--------------------------------------------------------------------------------
1 | import type { NativeProps } from "$utils";
2 | import type { Snippet } from "svelte";
3 | import type { HTMLAttributes } from "svelte/elements";
4 |
5 | export type QTableColumn = {
6 | name: string;
7 | required?: boolean;
8 | label: string;
9 | align?: "left" | "center" | "right";
10 | field: string | ((row: QTableRow) => string);
11 | format?: (val: string) => string;
12 | sortable?: boolean;
13 | sort?: (a: string, b: string) => number;
14 | };
15 |
16 | export type QTableRow = {
17 | [key: string]: string | number;
18 | };
19 |
20 | export type QTableSort = {
21 | columnField: string | ((row: QTableRow) => string);
22 | type: "asc" | "desc";
23 | } | null;
24 |
25 | export interface QTableProps extends NativeProps, HTMLAttributes {
26 | /**
27 | * Column definitions of the table.
28 | * @default []
29 | */
30 | columns: QTableColumn[];
31 |
32 | /**
33 | * Rows of the table.
34 | * @default []
35 | */
36 | rows: QTableRow[];
37 |
38 | /**
39 | * Uses flat design, removing the box-shadow around the table.
40 | * @default false
41 | */
42 | flat?: boolean;
43 |
44 | /**
45 | * Adds a border around the table.
46 | * @default false
47 | */
48 | bordered?: boolean;
49 |
50 | /**
51 | * Shows the Table in dense mode (takes up less space).
52 | * @default false
53 | */
54 | dense?: boolean;
55 |
56 | /**
57 | * Optionally pass a snippet to render each table cell.
58 | * @default undefined
59 | */
60 | bodyCell?: Snippet<[{ column: QTableColumn; row: QTableRow; style: string }]>;
61 | }
62 |
--------------------------------------------------------------------------------
/src/lib/components/breadcrumbs/QBreadcrumbs.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
41 |
42 |
50 |
51 | {@render children?.()}
52 |
53 |
54 |
--------------------------------------------------------------------------------
/src/lib/components/separator/QSeparator.svelte:
--------------------------------------------------------------------------------
1 |
37 |
38 |
39 | {#if text}
40 | {#if (vertical && textAlign !== "top") || (!vertical && textAlign !== "left")}
41 | {@render hr()}
42 | {/if}
43 |
44 |
{text}
45 |
46 | {#if (vertical && textAlign !== "bottom") || (!vertical && textAlign !== "right")}
47 | {@render hr()}
48 | {/if}
49 | {:else}
50 | {@render hr()}
51 | {/if}
52 |
53 |
54 | {#snippet hr()}
55 |
56 | {/snippet}
57 |
--------------------------------------------------------------------------------
/src/lib/css/mixins/_menu.scss:
--------------------------------------------------------------------------------
1 | @use "$css/variables";
2 |
3 | @mixin menu {
4 | opacity: 0;
5 | visibility: hidden;
6 | position: absolute;
7 | box-shadow: var(--elevate2);
8 | background-color: var(--surface);
9 | z-index: 11;
10 | top: auto;
11 | bottom: 0;
12 | left: 0;
13 | right: auto;
14 | width: 100%;
15 | max-height: 50vh;
16 | max-width: none !important;
17 | overflow-x: hidden;
18 | overflow-y: auto;
19 | font-size: 0.875rem;
20 | font-weight: normal;
21 | text-transform: none;
22 | color: var(--on-surface);
23 | line-height: normal;
24 | text-align: left;
25 | border-radius: 0.25rem;
26 | transform: scale(0.8) translateY(120%);
27 | transition:
28 | variables.$speed2 all,
29 | 0s background-color;
30 |
31 | &--active {
32 | opacity: 1;
33 | visibility: visible;
34 | transform: scale(1) translateY(100%);
35 | }
36 |
37 | * {
38 | white-space: inherit !important;
39 | }
40 |
41 | > a {
42 | padding: 0.75rem 1rem;
43 | border-radius: 0;
44 | }
45 |
46 | > a:not(.row) {
47 | display: block;
48 | }
49 |
50 | > a:is(:hover, :focus, .active) {
51 | background-color: var(--secondary-container);
52 | }
53 |
54 | &__dense {
55 | top: 0;
56 | bottom: auto;
57 | transform: none !important;
58 | border-radius: inherit;
59 | }
60 |
61 | &__maximized {
62 | position: fixed;
63 | top: 0;
64 | left: 0;
65 | bottom: 0;
66 | right: 0;
67 | height: 100%;
68 | max-height: none;
69 | min-height: auto;
70 | z-index: 100;
71 | border-radius: 0;
72 | transform: none !important;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/lib/css/theme/_typography.scss:
--------------------------------------------------------------------------------
1 | html {
2 | font-size: var(--size);
3 | }
4 |
5 | body {
6 | font-family: var(--font);
7 | font-size: 0.875rem;
8 | line-height: 1.25;
9 | }
10 |
11 | h1,
12 | h2,
13 | h3,
14 | h4,
15 | h5,
16 | h6 {
17 | font-weight: 400;
18 | display: flex;
19 | align-items: center;
20 | margin-bottom: 0.5rem;
21 | line-height: normal;
22 | }
23 |
24 | * + h1,
25 | * + h2,
26 | * + h3,
27 | * + h4,
28 | * + h5,
29 | * + h6 {
30 | margin-top: 1rem;
31 | }
32 |
33 | h1 {
34 | font-size: 3.5625rem;
35 | }
36 |
37 | h2 {
38 | font-size: 2.8125rem;
39 | }
40 |
41 | h3 {
42 | font-size: 2.25rem;
43 | }
44 |
45 | h4 {
46 | font-size: 2rem;
47 | }
48 |
49 | h5 {
50 | font-size: 1.75rem;
51 | }
52 |
53 | h6 {
54 | font-size: 1.5rem;
55 | }
56 |
57 | h1.small {
58 | font-size: 3.0625rem;
59 | }
60 |
61 | h2.small {
62 | font-size: 2.3125rem;
63 | }
64 |
65 | h3.small {
66 | font-size: 1.75rem;
67 | }
68 |
69 | h4.small {
70 | font-size: 1.5rem;
71 | }
72 |
73 | h5.small {
74 | font-size: 1.25rem;
75 | }
76 |
77 | h6.small {
78 | font-size: 1rem;
79 | }
80 |
81 | h1.large {
82 | font-size: 4.0625rem;
83 | }
84 |
85 | h2.large {
86 | font-size: 3.3125rem;
87 | }
88 |
89 | h3.large {
90 | font-size: 2.75rem;
91 | }
92 |
93 | h4.large {
94 | font-size: 2.5rem;
95 | }
96 |
97 | h5.large {
98 | font-size: 2.25rem;
99 | }
100 |
101 | h6.large {
102 | font-size: 2rem;
103 | }
104 |
105 | p {
106 | margin: 0.5rem 0;
107 | }
108 |
109 | .text-center {
110 | text-align: center;
111 | }
112 |
--------------------------------------------------------------------------------
/src/lib/components/icon/QIcon.scss:
--------------------------------------------------------------------------------
1 | .q-icon {
2 | --size: 1.5rem;
3 | overflow: visible;
4 | font-family: var(--font, "Material Symbols Outlined");
5 | font-weight: normal;
6 | font-style: normal;
7 | font-size: var(--size);
8 | letter-spacing: normal;
9 | text-transform: none;
10 | display: inline-flex;
11 | align-items: center;
12 | justify-content: center;
13 | white-space: nowrap;
14 | word-wrap: normal;
15 | direction: ltr;
16 | font-feature-settings: "liga";
17 | -webkit-font-smoothing: antialiased;
18 | vertical-align: middle;
19 | text-align: center;
20 | width: var(--size);
21 | min-width: var(--size);
22 | height: var(--size);
23 | min-height: var(--size);
24 | box-sizing: content-box;
25 | line-height: normal;
26 |
27 | &--outlined {
28 | --font: "Material Symbols Outlined";
29 | }
30 |
31 | &--rounded {
32 | --font: "Material Symbols Rounded";
33 | }
34 |
35 | &--sharp {
36 | --font: "Material Symbols Sharp";
37 | }
38 |
39 | &--xs {
40 | --size: 1rem;
41 | }
42 |
43 | &--sm {
44 | --size: 1.25rem;
45 | }
46 |
47 | &--lg {
48 | --size: 1.75rem;
49 | }
50 |
51 | &--xl {
52 | --size: 2rem;
53 | }
54 |
55 | > :is(img, svg) {
56 | width: 100%;
57 | height: 100%;
58 | background-size: 100%;
59 | border-radius: inherit;
60 | position: absolute;
61 | top: 0;
62 | left: 0;
63 | padding: inherit;
64 | }
65 |
66 | &--filled
67 | // we might need these selectors later:
68 | // a.row:is(:hover, :focus) > i,
69 | // .transparent:is(:hover, :focus) > i
70 | {
71 | font-variation-settings: "FILL" 1;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/lib/components/list/QList.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
55 |
56 |
57 | {@render children?.()}
58 |
59 |
--------------------------------------------------------------------------------
/src/lib/components/tabs/QTab.scss:
--------------------------------------------------------------------------------
1 | @use "$css/mixins";
2 |
3 | .q-tab {
4 | position: relative;
5 | box-sizing: border-box;
6 | min-height: 3rem;
7 | height: 3rem;
8 | min-width: max-content;
9 | display: flex;
10 | align-items: center;
11 | justify-content: center;
12 | flex: 1;
13 | background-color: var(--surface);
14 | color: var(--on-surface);
15 | border-radius: 0;
16 | gap: 0;
17 | overflow: visible;
18 | @include mixins.padding("x-md");
19 |
20 | & .q-tab__icon + span {
21 | margin-left: 0.5rem;
22 | }
23 |
24 | &.q-tab--active {
25 | color: var(--primary);
26 |
27 | & .q-tab__indicator {
28 | opacity: 1;
29 | }
30 | }
31 |
32 | &:is(:hover, :focus):not([aria-disabled])::after {
33 | content: "";
34 | position: absolute;
35 | top: 0;
36 | left: 0;
37 | width: 100%;
38 | height: 100%;
39 | background-color: var(--on-surface);
40 | border-radius: none;
41 | overflow: hidden;
42 | }
43 |
44 | &:hover:not([aria-disabled])::after {
45 | opacity: 0.08;
46 | }
47 |
48 | &:focus:not([aria-disabled])::after {
49 | opacity: 0.16;
50 | }
51 |
52 | & .q-tab__content {
53 | height: 100%;
54 | display: flex;
55 | align-items: center;
56 | justify-content: center;
57 | }
58 |
59 | & .q-tab__indicator {
60 | position: absolute;
61 | box-sizing: border-box;
62 | transform-origin: bottom left;
63 | background: var(--primary);
64 | border-radius: 0.1875rem 0.1875rem 0 0;
65 | height: 0.1875rem;
66 | inset: auto 0 0 0;
67 | // hidden unless the tab is selected
68 | opacity: 0;
69 | z-index: 1;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/lib/components/private/QDocsSection.svelte:
--------------------------------------------------------------------------------
1 |
30 |
31 |
32 |
33 |
{title}
34 | {#if code && !noCode}
35 | (dialog = true)} />
36 |
37 |
38 |
39 | {/if}
40 |
41 |
42 | {#if sectionDescription}
43 |
44 | {@render sectionDescription()}
45 |
46 | {/if}
47 |
48 |
49 | {@render children?.()}
50 |
51 |
52 |
--------------------------------------------------------------------------------
/src/lib/css/mixins/_design.scss:
--------------------------------------------------------------------------------
1 | @use "sass:list";
2 | @use "../variables";
3 |
4 | @mixin elevate($strength, $position: null) {
5 | $coeff: 1;
6 | @if $position == "top" {
7 | $coeff: -1;
8 | }
9 | $elevate: 0.125rem * $strength;
10 |
11 | box-shadow: 0 $elevate * $coeff $elevate 0 with-alpha(var(--shadow), calc(24% + (8% * $strength)));
12 | }
13 |
14 | @mixin border($color: var(--outline), $position: variables.$border-positions) {
15 | @if list.index($list: $position, $value: top) {
16 | border-top: 0.0625rem solid $color;
17 | }
18 |
19 | @if list.index($list: $position, $value: right) {
20 | border-right: 0.0625rem solid $color;
21 | }
22 |
23 | @if list.index($list: $position, $value: bottom) {
24 | border-bottom: 0.0625rem solid $color;
25 | }
26 |
27 | @if list.index($list: $position, $value: left) {
28 | border-left: 0.0625rem solid $color;
29 | }
30 | }
31 |
32 | @mixin transparent($ripple: false) {
33 | background-color: transparent;
34 | background: transparent;
35 | color: inherit;
36 |
37 | &:is(:focus, :hover) > .q-icon {
38 | font-variation-settings: "FILL" 1;
39 | }
40 |
41 | @if $ripple {
42 | &::after {
43 | background-image: radial-gradient(circle, rgb(150, 150, 150, 0.2) 1%, transparent 1%);
44 | }
45 | }
46 | }
47 |
48 | @mixin overlay($color: null, $opacity: null) {
49 | content: "";
50 | position: absolute;
51 | top: 0;
52 | right: 0;
53 | bottom: 0;
54 | left: 0;
55 |
56 | border-radius: inherit;
57 |
58 | background-color: $color;
59 | opacity: $opacity;
60 | }
61 |
62 | @function with-alpha($color-variable: null, $percentage: null) {
63 | @return color-mix(in srgb, $color-variable $percentage, transparent);
64 | }
65 |
--------------------------------------------------------------------------------
/src/lib/css/mixins/_field.scss:
--------------------------------------------------------------------------------
1 | @use "sass:math";
2 |
3 | @mixin reset-form-element {
4 | padding: 0px;
5 | background: none;
6 | border: none;
7 | border-radius: 0px;
8 | outline: none;
9 | appearance: none;
10 | font-family: inherit;
11 | }
12 |
13 | @mixin clip-border($clip-width, $clip-height) {
14 | clip-path: polygon(
15 | 0% 0%,
16 | 0% 100%,
17 | 100% 100%,
18 | 100% $clip-height,
19 | calc(100% - #{$clip-width}) $clip-height,
20 | calc(100% - #{$clip-width}) 0%
21 | );
22 | }
23 |
24 | @mixin border-substitute($variant: "outlined", $wrapper-height, $dense: false) {
25 | $border-radius: if($variant == "outlined", 0.25rem, math.div($wrapper-height, 2));
26 |
27 | &::before,
28 | &::after {
29 | content: "";
30 | position: absolute;
31 | top: -0.0625rem;
32 | bottom: -0.0625rem;
33 | width: math.div($wrapper-height, 2);
34 | height: $wrapper-height;
35 | border: 0.0625rem solid transparent;
36 | border-top-color: var(--decorator-color);
37 | border-radius: $border-radius;
38 | box-sizing: border-box;
39 | }
40 |
41 | &::before {
42 | left: -0.0625rem;
43 | border-top-right-radius: 0px;
44 | border-bottom-right-radius: 0px;
45 | $clip-height: 0.25rem;
46 | $clip-width: if($variant == "outlined", 0.9375rem, 0.25rem);
47 |
48 | @if $dense == true and $variant == "outlined" {
49 | $clip-width: math.div($clip-width, 1.4);
50 | }
51 |
52 | @if $dense == true and $variant != "outlined" {
53 | $clip-width: 0rem;
54 | }
55 |
56 | @include clip-border($clip-width, $clip-height);
57 | }
58 |
59 | &::after {
60 | right: -0.0625rem;
61 | border-top-left-radius: 0px;
62 | border-bottom-left-radius: 0px;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/lib/components/progress/QLinearProgress.scss:
--------------------------------------------------------------------------------
1 | @use "$css/mixins";
2 |
3 | .q-linear-progress {
4 | position: relative;
5 |
6 | flex-grow: 1;
7 | height: 0.75em;
8 |
9 | overflow: hidden;
10 |
11 | transform: scale3d(1, 1, 1);
12 |
13 | &__indicator {
14 | background-color: var(--q-indicator-color);
15 | height: 100%;
16 |
17 | &--indeterminate {
18 | transition: none;
19 | background-color: transparent;
20 |
21 | &::before,
22 | &::after {
23 | content: "";
24 | position: absolute;
25 | top: 0;
26 | right: 0;
27 | bottom: 0;
28 | left: 0;
29 |
30 | background-color: var(--q-indicator-color);
31 | transform-origin: 0 0;
32 | border-radius: inherit;
33 | }
34 |
35 | &::before {
36 | animation: q-progress 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;
37 | }
38 |
39 | &::after {
40 | transform: translate3d(-101%, 0, 0) scale3d(1, 1, 1);
41 | animation: q-progress-fast 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;
42 | animation-delay: 1.15s;
43 | }
44 | }
45 | }
46 |
47 | &__track {
48 | opacity: 0.4;
49 | height: 100%;
50 | }
51 | }
52 |
53 | @keyframes q-progress {
54 | 0% {
55 | transform: translate3d(-35%, 0, 0) scale3d(0.35, 1, 1);
56 | }
57 | 60% {
58 | transform: translate3d(100%, 0, 0) scale3d(0.9, 1, 1);
59 | }
60 | 100% {
61 | transform: translate3d(100%, 0, 0) scale3d(0.9, 1, 1);
62 | }
63 | }
64 |
65 | @keyframes q-progress-fast {
66 | 0% {
67 | transform: translate3d(-101%, 0, 0) scale3d(1, 1, 1);
68 | }
69 | 60% {
70 | transform: translate3d(107%, 0, 0) scale3d(0.01, 1, 1);
71 | }
72 | 100% {
73 | transform: translate3d(107%, 0, 0) scale3d(0.01, 1, 1);
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/lib/utils/props.ts:
--------------------------------------------------------------------------------
1 | import { convertCase } from "./string";
2 |
3 | export function createStyles(
4 | styleObj: Record,
5 | userStyles?: string | null
6 | ) {
7 | const stylesArray = Object.entries(styleObj);
8 | const toJoin: string[] = [];
9 |
10 | for (const [styleName, styleVal] of stylesArray) {
11 | if (styleVal === undefined || styleVal === null || styleVal === false) {
12 | continue;
13 | }
14 |
15 | toJoin.push(`${convertCase(styleName, "camel", "kebab")}: ${styleVal}`);
16 | }
17 |
18 | if (userStyles) {
19 | toJoin.push(userStyles);
20 | }
21 |
22 | if (toJoin.length === 0) {
23 | return null;
24 | }
25 |
26 | return toJoin.join("; ");
27 | }
28 |
29 | interface CreateClassesOptions {
30 | component?: string;
31 | element?: string;
32 | userClasses?: string | null;
33 | quaffClasses?: unknown[];
34 | }
35 |
36 | export function createClasses(
37 | modifiers: unknown[],
38 | options: CreateClassesOptions = { userClasses: "", quaffClasses: [] }
39 | ): string {
40 | const userClasses = options.userClasses?.trim();
41 | const quaffClasses = options.quaffClasses?.length && createClasses(options.quaffClasses);
42 | const baseClasses = `${quaffClasses || ""} ${userClasses || ""}`.trim();
43 |
44 | const component = options.component;
45 | let element: string | undefined;
46 |
47 | if (component && options.element) {
48 | element = `${component}__${options.element}`;
49 | }
50 |
51 | const filteredModifiers = modifiers.filter(Boolean);
52 |
53 | const withModifiers = component
54 | ? filteredModifiers.map((modifier) => `${element || component}--${modifier}`)
55 | : filteredModifiers;
56 |
57 | return [element || component, ...withModifiers, baseClasses].filter(Boolean).join(" ").trim();
58 | }
59 |
--------------------------------------------------------------------------------
/src/dev/waitForSvelteKit.ts:
--------------------------------------------------------------------------------
1 | import { watch } from "fs";
2 | import { dirname, resolve as resolvePath } from "path";
3 |
4 | type PromiseResolve = (value: void | PromiseLike) => void;
5 | type PromiseWithResolve = Promise & { resolve: PromiseResolve };
6 |
7 | function makeResolvablePromise() {
8 | let resolvePromise: PromiseResolve = () => {};
9 |
10 | const p = new Promise((resolve) => {
11 | resolvePromise = resolve;
12 | });
13 |
14 | return Object.assign(p, { resolve: resolvePromise }) as Omit & {
15 | resolve?: PromiseWithResolve["resolve"];
16 | };
17 | }
18 |
19 | const TIMEOUT_MS = 30 * 1000;
20 |
21 | export default async function waitForSvelteKit({
22 | svelteKitPathResolved,
23 | svelteKitTsconfigPathResolved,
24 | }: {
25 | svelteKitPathResolved: string;
26 | svelteKitTsconfigPathResolved: string;
27 | }) {
28 | const cancelWatcher = new AbortController();
29 | const promise = makeResolvablePromise();
30 |
31 | const timeout = setTimeout(() => {
32 | console.warn("timeout reached. this is probably a bug.");
33 | promise.resolve?.();
34 | }, TIMEOUT_MS);
35 |
36 | watch(
37 | dirname(svelteKitPathResolved),
38 | {
39 | recursive: true,
40 | signal: cancelWatcher.signal,
41 | },
42 | (eventType, fileName) => {
43 | if (!fileName?.includes("tsconfig.json")) {
44 | return;
45 | }
46 |
47 | const pathResolved = resolvePath(fileName);
48 |
49 | if (pathResolved === svelteKitTsconfigPathResolved) {
50 | cancelWatcher.abort();
51 |
52 | if (!promise.resolve) {
53 | throw new Error("expected promise to have a resolve property");
54 | }
55 |
56 | clearInterval(timeout);
57 | promise.resolve();
58 | }
59 | }
60 | );
61 |
62 | return await promise;
63 | }
64 |
--------------------------------------------------------------------------------
/docgen/props/worker.ts:
--------------------------------------------------------------------------------
1 | import { writeFile } from "fs/promises";
2 | import { MessagePort, parentPort } from "worker_threads";
3 | import parseTypes from "../types/parseTypes.js";
4 | import formatCodeAndAddHash from "../helpers/formatCodeAndAddHash.js";
5 | import parseInterface from "./parseInterface.js";
6 |
7 | function assertHasParentPort(parentPort: MessagePort | null): asserts parentPort is MessagePort {
8 | if (!parentPort) {
9 | throw new Error("missing parentPort");
10 | }
11 | }
12 |
13 | assertHasParentPort(parentPort);
14 |
15 | parentPort.on("message", async (workerData) => {
16 | const { propsFilePath, docsPropsFilePath, hashProps } = workerData;
17 |
18 | console.log("processing", propsFilePath);
19 | const parsedInterface = parseInterface(propsFilePath);
20 | const types = await parseTypes(propsFilePath);
21 |
22 | let contents = 'import { ParsedProp, ParsedSnippet } from "$docgen/props/parseInterface"\n\n';
23 |
24 | Object.keys(parsedInterface).forEach((varName) => {
25 | const interfaceResults = parsedInterface[varName];
26 |
27 | const props = interfaceResults.filter((prop) => !prop.isSnippet);
28 | const snippets = interfaceResults.filter((prop) => prop.isSnippet);
29 |
30 | contents += `export const ${varName.replace(/Props$/, "DocsProps")}: ParsedProp[] = ${JSON.stringify(
31 | props,
32 | null,
33 | 2
34 | )};\n\n`;
35 |
36 | contents += `export const ${varName.replace(/Props$/, "DocsSnippets")}: ParsedSnippet[] = ${JSON.stringify(
37 | snippets,
38 | null,
39 | 2
40 | )};\n\n`;
41 | });
42 |
43 | const formattedWithComment = await formatCodeAndAddHash(contents, hashProps);
44 |
45 | await writeFile(docsPropsFilePath, formattedWithComment, "utf8");
46 |
47 | assertHasParentPort(parentPort);
48 |
49 | parentPort.postMessage({ event: "finished", types });
50 | });
51 |
--------------------------------------------------------------------------------
/docgen/props/updateAllProps.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import { fileURLToPath } from "url";
3 | import getComponentDirs from "../helpers/getComponentDirs.js";
4 | import pathExists from "../helpers/pathExists.js";
5 | import getInfo from "./getInfo.js";
6 | import WorkerManager from "./WorkerManager.js";
7 | import updateDocTypesFile from "./updateDocTypesFile.js";
8 | import type { WorkerTask } from "./WorkerManager.js";
9 |
10 | const dirname = path.dirname(fileURLToPath(import.meta.url));
11 | const rootDir = path.resolve(dirname, "../../src/lib/components");
12 | const docTypesPath = path.resolve(dirname, "../../src/lib/utils/types.json");
13 |
14 | let didUpdateAllFiles = true;
15 |
16 | const workerPath = path.resolve(dirname, "./worker.ts");
17 |
18 | export default async function updateAllProps() {
19 | const componentDirs = await getComponentDirs(rootDir);
20 | const needsToRunAll = !(await pathExists(docTypesPath));
21 | const tasks: WorkerTask[] = [];
22 |
23 | for (const dir of componentDirs) {
24 | const propsFilePath = path.resolve(rootDir, dir, "props.ts");
25 | const docsPropsFilePath = path.resolve(rootDir, dir, "docs.props.ts");
26 | const { needsToBeGenerated, hashProps } = await getInfo(propsFilePath, docsPropsFilePath);
27 |
28 | if (needsToBeGenerated || (needsToRunAll && hashProps)) {
29 | tasks.push({
30 | propsFilePath,
31 | docsPropsFilePath,
32 | hashProps,
33 | });
34 | }
35 |
36 | if (!needsToBeGenerated && hashProps && !needsToRunAll) {
37 | didUpdateAllFiles = false;
38 | }
39 | }
40 |
41 | if (!tasks.length) {
42 | process.exit(0);
43 | }
44 |
45 | const workerManager = new WorkerManager(workerPath, tasks);
46 |
47 | workerManager.on("finished", async (types: Record) => {
48 | await updateDocTypesFile(docTypesPath, types, didUpdateAllFiles);
49 | });
50 | }
51 |
--------------------------------------------------------------------------------
/src/lib/components/layout/props.ts:
--------------------------------------------------------------------------------
1 | import type { NativeProps } from "$utils";
2 | import type { Snippet } from "svelte";
3 | import type { HTMLAttributes } from "svelte/elements";
4 |
5 | //prettier-ignore
6 | export type QLayoutViewOptions = `${"l"|"h"}${"h"|"H"}${"r"|"h"} ${"l"|"L"}${"p"}${"r"|"R"} ${"l"|"f"}${"f"|"F"}${"r"|"f"}`
7 |
8 | export interface QLayoutProps extends NativeProps, HTMLAttributes {
9 | /**
10 | * The layout view configuration, which defines how layout components (header, railbars, drawers, footer) should be displayed on screen.
11 | * See Quasar's explanation on the view prop .
12 | * @default "hhh lpr fff"
13 | */
14 | view?: QLayoutViewOptions;
15 |
16 | /**
17 | * Main area of the layout where the content will be displayed, meaning everything besides the layout components (header, railbars, drawers, footer).
18 | * It overrides the default children snippet.
19 | * @default undefined
20 | */
21 | content?: Snippet;
22 |
23 | /**
24 | * The railbar on the left side of the layout.
25 | * @default undefined
26 | */
27 | railbarLeft?: Snippet;
28 |
29 | /**
30 | * The railbar on the right side of the layout.
31 | * @default undefined
32 | */
33 | railbarRight?: Snippet;
34 |
35 | /**
36 | * The drawer on the left side of the layout.
37 | * @default undefined
38 | */
39 | drawerLeft?: Snippet;
40 |
41 | /**
42 | * The drawer on the right side of the layout.
43 | * @default undefined
44 | */
45 | drawerRight?: Snippet;
46 |
47 | /**
48 | * The header of the layout.
49 | * @default undefined
50 | */
51 | header?: Snippet;
52 |
53 | /**
54 | * The footer of the layout.
55 | * @default undefined
56 | */
57 | footer?: Snippet;
58 | }
59 |
60 | export type QLayoutEvents = "resize" | "scroll" | "scrollHeight";
61 |
--------------------------------------------------------------------------------
/src/routes/colors/+page.svelte:
--------------------------------------------------------------------------------
1 |
36 |
37 |
38 | {pageTitle("Colors")}
39 |
40 |
41 |
42 |
Colors
43 |
44 |
45 | {#each colors as color (color)}
46 |
47 |
{color}
48 | {#each { length: 10 }, index (index)}
49 | {@const shadeNumber = index + 1}
50 |
51 | {color}-{shadeNumber}
52 |
53 | {/each}
54 |
55 | {/each}
56 |
57 |
58 |
59 |
75 |
--------------------------------------------------------------------------------
/src/lib/css/_variables.scss:
--------------------------------------------------------------------------------
1 | // Responsive breakpoints
2 | $breakpoints: (
3 | xs: 0px,
4 | sm: 600px,
5 | md: 960px,
6 | lg: 1280px,
7 | xl: 1920px,
8 | );
9 |
10 | // Grid variables
11 | $grid-columns: 12;
12 |
13 | // Spaces
14 | $space-base: 1rem !default;
15 | $space-x-base: $space-base !default;
16 | $space-y-base: $space-base !default;
17 |
18 | $space-none: 0 !default;
19 | $space-xs: 0.25rem !default;
20 | $space-sm: 0.5rem !default;
21 | $space-md: 1rem !default;
22 | $space-lg: 1.5rem !default;
23 | $space-xl: 3rem !default;
24 |
25 | $spaces: (
26 | "auto": auto,
27 | "none": $space-none,
28 | "xs": $space-xs,
29 | "sm": $space-sm,
30 | "md": $space-md,
31 | "lg": $space-lg,
32 | "xl": $space-xl,
33 | ) !default;
34 |
35 | // Positions
36 | $positions: (
37 | "t": -top,
38 | "r": -right,
39 | "b": -bottom,
40 | "l": -left,
41 | "x": -inline,
42 | "y": -block,
43 | "a": "",
44 | );
45 | $border-positions: top, right, bottom, left;
46 |
47 | // Speeds
48 | $speed1: 0.1s;
49 | $speed2: 0.2s;
50 | $speed3: 0.3s;
51 | $speed4: 0.4s;
52 |
53 | // Layout offsets
54 | $left-railbar: var(--left-railbar-width);
55 | $right-railbar: var(--right-railbar-width);
56 | $left-right-railbar: calc(#{$left-railbar} + #{$right-railbar});
57 |
58 | $left-drawer: var(--left-drawer-width);
59 | $right-drawer: var(--right-drawer-width);
60 | $left-right-drawer: calc(#{$left-drawer} + #{$right-drawer});
61 |
62 | $left-full: calc(#{$left-railbar} + #{$left-drawer});
63 | $right-full: calc(#{$right-railbar} + #{$right-drawer});
64 |
65 | $left-railbar-right-drawer: calc(#{$left-railbar} + #{$right-drawer});
66 | $right-railbar-left-drawer: calc(#{$right-railbar} + #{$left-drawer});
67 |
68 | $left-full-right-railbar: calc(#{$left-full} + #{$right-railbar});
69 | $left-full-right-drawer: calc(#{$left-full} + #{$right-drawer});
70 | $right-full-left-railbar: calc(#{$right-full} + #{$left-railbar});
71 | $right-full-left-drawer: calc(#{$right-full} + #{$left-drawer});
72 |
73 | $full: calc(#{$left-full} + #{$right-full});
74 |
--------------------------------------------------------------------------------
/src/lib/components/list/QItemSection.svelte:
--------------------------------------------------------------------------------
1 |
44 |
45 |
46 | {#if type === "content"}
47 | {#if !headline && !line1 && !line2 && !line3}
48 | {@render children?.()}
49 | {:else}
50 | {@render line(headline)}
51 |
52 | {@render line(line1)}
53 |
54 | {@render line(line2)}
55 |
56 | {@render line(line3)}
57 | {/if}
58 | {:else if type === "trailingText"}
59 |
60 | {@render children?.()}
61 |
62 | {:else}
63 |
64 | {@render children?.()}
65 |
66 | {/if}
67 |
68 |
69 | {#snippet line(snip: Snippet | undefined)}
70 | {#if snip}
71 |
72 | {@render snip()}
73 |
74 | {/if}
75 | {/snippet}
76 |
--------------------------------------------------------------------------------
/src/lib/components/avatar/index.scss:
--------------------------------------------------------------------------------
1 | @use "$css/variables";
2 |
3 | //? Basic styles
4 | .q-avatar {
5 | object-fit: cover;
6 | object-position: center;
7 | aspect-ratio: 1;
8 | transition:
9 | variables.$speed3 transform,
10 | variables.$speed3 border-radius,
11 | variables.$speed3 padding;
12 | border-radius: 0;
13 |
14 | display: flex;
15 | align-items: center;
16 | justify-content: center;
17 |
18 | font-size: 1rem;
19 | text-transform: uppercase;
20 |
21 | overflow: hidden;
22 |
23 | -webkit-user-select: none;
24 | -moz-user-select: none;
25 | -ms-user-select: none;
26 | user-select: none;
27 |
28 | &--circle {
29 | margin: 0;
30 | }
31 | }
32 |
33 | //? Sizes
34 | $sizes: (
35 | "xs": 1,
36 | "sm": 2,
37 | "md": 3,
38 | "lg": 4,
39 | "xl": 5,
40 | );
41 |
42 | .q-avatar {
43 | @each $size, $val in $sizes {
44 | &--#{$size} {
45 | height: 2rem + ($val - 1) * 0.5rem;
46 | width: 2rem + ($val - 1) * 0.5rem;
47 | }
48 | }
49 | }
50 |
51 | //? Border radius
52 | $positions-y: "top", "bottom";
53 | $positions-x: "right", "left";
54 |
55 | .q-avatar {
56 | &--top-right-round {
57 | border-top-right-radius: 50%;
58 | }
59 | &--top-left-round {
60 | border-top-left-radius: 50%;
61 | }
62 | &--bottom-right-round {
63 | border-bottom-right-radius: 50%;
64 | }
65 | &--bottom-left-round {
66 | border-bottom-left-radius: 50%;
67 | }
68 |
69 | @each $pos-y in $positions-y {
70 | &--#{$pos-y}-round {
71 | @extend .q-avatar--#{$pos-y}-left-round;
72 | @extend .q-avatar--#{$pos-y}-right-round;
73 | }
74 | }
75 |
76 | @each $pos-x in $positions-x {
77 | &--#{$pos-x}-round {
78 | @extend .q-avatar--top-#{$pos-x}-round;
79 | @extend .q-avatar--bottom-#{$pos-x}-round;
80 | }
81 | }
82 |
83 | &--circle {
84 | @extend .q-avatar--top-left-round;
85 | @extend .q-avatar--top-right-round;
86 | @extend .q-avatar--bottom-left-round;
87 | @extend .q-avatar--bottom-right-round;
88 | }
89 |
90 | &--round {
91 | border-radius: 0.5rem;
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/routes/privacy-policy/+page.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 | {pageTitle("Privacy Policy")}
7 |
8 |
9 |
10 |
11 |
Privacy Policy
12 |
13 |
14 |
Last updated May 12, 2025
15 |
16 |
17 | Quaff (open-source project at github.com/quaffui/quaff ) is the data controller.
20 |
21 |
22 |
23 | We do not collect, process, or retain any personal data—no cookies, analytics, third‑party
24 | embeds, or scripts. Accordingly, GDPR access, rectification, and erasure rights do not
25 | apply. You may nevertheless lodge a complaint with your local supervisory authority.
26 |
27 |
28 |
29 | This static page is served via GitHub Pages; server logs are processed by GitHub under its Privacy Statement .
33 |
34 |
35 |
49 |
50 |
51 | For privacy questions, open an issue at github.com/quaffui/quaff .
54 |
55 |
56 |
This policy may be updated; please check back regularly.
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/plugins/class-preprocessor/markup.ts:
--------------------------------------------------------------------------------
1 | import type { Fragment, ComponentName, ClassesUsage } from "./types.js";
2 |
3 | /**
4 | * Prepares the positions to be able to modify the markup
5 | */
6 | export function prepareMarkup(
7 | fragment: Fragment,
8 | component: ComponentName,
9 | uses: ClassesUsage[],
10 | namespace: string
11 | ) {
12 | for (const node of fragment.nodes) {
13 | if (node.type === "IfBlock") {
14 | prepareMarkup(node.consequent, component, uses, namespace);
15 |
16 | if (node.alternate) {
17 | prepareMarkup(node.alternate, component, uses, namespace);
18 | }
19 |
20 | continue;
21 | } else if (node.type === "AwaitBlock") {
22 | for (const awaitKey of ["pending", "then", "catch"] as const) {
23 | const frag = node[awaitKey];
24 | if (frag) {
25 | prepareMarkup(frag, component, uses, namespace);
26 | }
27 | }
28 |
29 | continue;
30 | } else if (node.type === "EachBlock" || node.type === "SnippetBlock") {
31 | prepareMarkup(node.body, component, uses, namespace);
32 |
33 | continue;
34 | }
35 |
36 | if (!("fragment" in node)) {
37 | continue;
38 | }
39 |
40 | prepareMarkup(node.fragment, component, uses, namespace);
41 |
42 | if (!("attributes" in node)) {
43 | continue;
44 | }
45 |
46 | const classAttribute = node.attributes.find(
47 | (attr) => attr.type === "Attribute" && attr.name === "class"
48 | ) as ((typeof node.attributes)[0] & { type: "Attribute" }) | undefined;
49 |
50 | if (
51 | !classAttribute ||
52 | !Array.isArray(classAttribute.value) ||
53 | classAttribute.value.length !== 1
54 | ) {
55 | continue;
56 | }
57 |
58 | const cls = classAttribute.value[0];
59 | if (cls.type !== "Text" || cls.data !== component) {
60 | continue;
61 | }
62 | // Elements to attach the classes to
63 |
64 | // We grab the start and end of the class definition to replace with the new classes
65 | const { start, end } = cls;
66 |
67 | uses.push({
68 | start,
69 | end,
70 | });
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/lib/components/chip/props.ts:
--------------------------------------------------------------------------------
1 | import type { MaterialSymbol } from "material-symbols";
2 | import type { HTMLAttributes, MouseEventHandler } from "svelte/elements";
3 |
4 | export type QChipKindOptions = "assist" | "filter" | "input" | "suggestion";
5 | export type QChipFillOptions =
6 | | "primary"
7 | | "secondary"
8 | | "tertiary"
9 | | "neutral"
10 | | "neutral-variant"
11 | | "error";
12 |
13 | export type QChipSizeOptions = Exclude;
14 |
15 | export interface QChipProps extends HTMLAttributes {
16 | /**
17 | * The chip's kind. It will control the chip's style and behavior.
18 | * @default undefined
19 | */
20 | kind?: QChipKindOptions;
21 |
22 | /**
23 | * The chip's text content. Will overwrite the default slot.
24 | * @default undefined
25 | */
26 | label?: string;
27 |
28 | /**
29 | * Name of the leading icon to use for the chip. If starts with "img:", will be used as an image src instead.
30 | * @default undefined
31 | */
32 | icon?: MaterialSymbol | `img:${string}`;
33 |
34 | /**
35 | * Only for filter and input chips. Name of the trailing icon to use for the chip.
36 | * @default undefined
37 | */
38 | trailingIcon?: MaterialSymbol | `img:${string}`;
39 |
40 | /**
41 | * Puts the chip in a disabled state, making it unactivable.
42 | * @default false
43 | */
44 | disabled?: boolean;
45 |
46 | /**
47 | * Only for filter chips. Controls wether the chip is selected or not.
48 | * @default false
49 | */
50 | selected?: boolean;
51 |
52 | /**
53 | * Elevates the button, giving it box-shadow and a background color.
54 | * @default false
55 | */
56 | elevated?: boolean;
57 |
58 | /**
59 | * Disable the ripple effect for the chip.
60 | * @default false
61 | */
62 | noRipple?: boolean;
63 |
64 | /**
65 | * Size of the chip.
66 | * @default sm
67 | */
68 | size?: QChipSizeOptions;
69 |
70 | /**
71 | * Click event handler for the trailing icon of the chip. This can be useful with input chips to clear them.
72 | * @default undefined
73 | */
74 | onTrailingIconClick?: MouseEventHandler;
75 | }
76 |
--------------------------------------------------------------------------------
/src/lib/components/railbar/QRailbar.svelte:
--------------------------------------------------------------------------------
1 |
64 |
65 |
66 | {@render children?.()}
67 |
68 |
--------------------------------------------------------------------------------
/src/routes/grid/+page.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 | {pageTitle("Grid")}
9 |
10 |
11 |
12 |
Grid system
13 |
14 |
15 | Quaff provides a basic 12 column grid system with responsive support. Under the hood, we're
16 | using display: grid, providing you a modern grid without any hacks.
17 |
18 |
19 |
20 | {#each cols as col (col)}
21 |
24 | col-xs-12 col-md-6 col-lg-3
25 |
26 | {/each}
27 |
28 |
29 |
Gutter
30 |
31 |
32 | You can add space between your columns using the q-gutter-* classes, where * is either xs, sm,
33 | md, lg, xl or none.
34 |
35 |
36 | {#each ["xs", "sm", "md", "lg", "xl", "none"] as size (size)}
37 |
{size}
38 |
39 |
40 |
col-6
41 |
col-6
42 |
col-4
43 |
col-4
44 |
col-4
45 |
col-3
46 |
col-3
47 |
col-3
48 |
col-3
49 |
50 | {/each}
51 |
52 |
Breakpoints
53 |
54 |
55 | {#each [["xs", 0, 599], ["sm", 600, 959], ["md", 960, 1279], ["lg", 1280, 1919], ["xl", 1920]] as info (info)}
56 |
57 | {info[0]}
58 | {#if info.length === 3}
59 | {info[1]}-{info[2]}px
60 | {:else}
61 | {info[1]}px and larger
62 | {/if}
63 |
64 | {/each}
65 |
66 |
67 |
--------------------------------------------------------------------------------
/src/lib/components/breadcrumbs/props.ts:
--------------------------------------------------------------------------------
1 | import type { MaterialSymbol } from "material-symbols";
2 | import type { Snippet } from "svelte";
3 | import type { HTMLAttributes } from "svelte/elements";
4 |
5 | export type QBreadcrumbsGutterOptions = Exclude;
6 |
7 | export interface QBreadcrumbsProps extends HTMLAttributes {
8 | /**
9 | * Color to use for the active breadcrumb element. See to see what colors can be used.
10 | * @default "primary"
11 | */
12 | activeColor?: string;
13 | /**
14 | * Space around separators.
15 | * @default "sm"
16 | */
17 | gutter?: QBreadcrumbsGutterOptions;
18 | /**
19 | * Separator to use between the breadcrumb elements. To use an icon, prefix with "icon:" followed by the name of the icon.
20 | * @default "/"
21 | */
22 | separator?: Q.StringWithSuggestions<`icon:${MaterialSymbol}`> | Snippet;
23 | /**
24 | * Color to use for the separators. See to see what colors can be used.
25 | * @default "outline"
26 | */
27 | separatorColor?: string;
28 | }
29 |
30 | export interface QBreadcrumbsElProps extends HTMLAttributes {
31 | /**
32 | * Class to apply to the breadcrumb element when the route is active.
33 | * @default "active"
34 | */
35 | activeClass?: string;
36 | /**
37 | * Name of the leading icon for the breadcrumb element. The icon prop overwrites to icon slot.
38 | * @default undefined
39 | */
40 | icon?: MaterialSymbol | Snippet;
41 | /**
42 | * Text to use for the breadcrumb element. If default slot is defined, the label will be overwritten by it.
43 | * @default ""
44 | */
45 | label?: string;
46 | /**
47 | * Also makes the breadcrumb element navigational. Can be used with the router (e.g to="/home") or as a normal href attribute (e.g to="#section-id")
48 | * @default undefined
49 | */
50 | href?: string;
51 | /**
52 | * Tag to use for the breadcrumb element.
53 | * @default "span"
54 | */
55 | tag?: string;
56 | /**
57 | * Makes the breadcrumb element navigational. Can be used with the router (e.g to="/home") or as a normal href attribute (e.g to="#section-id")
58 | * @default undefined
59 | */
60 | to?: string;
61 | }
62 |
--------------------------------------------------------------------------------
/plugins/class-preprocessor/index.ts:
--------------------------------------------------------------------------------
1 | import { parse } from "svelte/compiler";
2 | import MagicString from "magic-string";
3 | import { format } from "prettier";
4 | import { prepareScript } from "./script.js";
5 | import { prepareMarkup } from "./markup.js";
6 | import { changeSource } from "./source.js";
7 | import type { PreprocessorGroup } from "svelte/compiler";
8 | import type { ComponentName, ClassesUsage } from "./types.js";
9 |
10 | /**
11 | * Preprocessor to ease the writing of classes following the BEM principle.
12 | * For it to work, a `${namespace}.classes` global function type should be defined.
13 | *
14 | * @example
15 | * ```js
16 | * ${namespace}.classes("q-component__name", {
17 | * dynamicClass1,
18 | * dynamicClass2,
19 | * dynamicClass3Renamed: dynamicClass3,
20 | * }, ["static-class", staticClassButFromVar, () => avoidSvelteWarningForDerivedVars])
21 | * ```
22 | *
23 | * @param namespace The global namespace to recognise the function `${namespace}.classes()`
24 | *
25 | */
26 | export function preprocessClasses(namespace: string): PreprocessorGroup {
27 | return {
28 | name: "quaff-classes-preprocessor",
29 | async markup({ content, filename }) {
30 | const source = new MagicString(content);
31 |
32 | const parsed = parse(content, { modern: true, filename });
33 |
34 | // Force "parsed" to be of type `Root`
35 | if (!("fragment" in parsed)) {
36 | return;
37 | }
38 |
39 | const { fragment, instance } = parsed;
40 | if (!instance) {
41 | return;
42 | }
43 |
44 | const scriptDefs = prepareScript(instance, content, namespace);
45 |
46 | for (const component in scriptDefs) {
47 | const def = scriptDefs[component];
48 | const uses: ClassesUsage[] = [];
49 | prepareMarkup(fragment, component as ComponentName, uses, namespace);
50 |
51 | const result = { def, uses };
52 |
53 | changeSource(source, result);
54 | }
55 |
56 | const code = await format(source.toString(), {
57 | plugins: ["prettier-plugin-svelte"],
58 | filepath: filename,
59 | });
60 |
61 | return {
62 | code,
63 | map: source.generateMap({ hires: true }),
64 | filename,
65 | };
66 | },
67 | };
68 | }
69 |
--------------------------------------------------------------------------------
/src/lib/classes/QTheme.svelte.ts:
--------------------------------------------------------------------------------
1 | import { generateColors, type HexValue, type QuaffColors, type Mode } from "$utils";
2 |
3 | type ThemeColors = Record<`${keyof QuaffColors}-${Mode}`, HexValue>;
4 |
5 | function extractColorFromCssVar(cssVar: string) {
6 | const rootStyles = getComputedStyle(document.documentElement);
7 | // remove var(...) to get only the color
8 | const varName = cssVar.replace(/var\(([a-z0-9-]+)\)/, "$1");
9 | return rootStyles.getPropertyValue(varName).trim();
10 | }
11 |
12 | function prepareThemeColors(from: string) {
13 | if (from.startsWith("var(")) {
14 | from = extractColorFromCssVar(from);
15 | }
16 |
17 | const theme = generateColors(from);
18 |
19 | //@ts-expect-error The properties are added in the next for loop
20 | const themeColors: ThemeColors = {};
21 |
22 | let mode: "light" | "dark";
23 | for (mode in theme) {
24 | let color: keyof QuaffColors;
25 | for (color in theme[mode]) {
26 | const cssColor: keyof typeof themeColors = `${color}-${mode}`;
27 | themeColors[cssColor] = theme[mode][color];
28 | }
29 | }
30 |
31 | return themeColors;
32 | }
33 |
34 | class QTheme {
35 | themeColors = $state({} as ThemeColors);
36 | srcColor = $state("#0039b4");
37 |
38 | constructor() {
39 | this.themeColors = prepareThemeColors(this.srcColor);
40 | }
41 |
42 | private apply() {
43 | const root = document.documentElement;
44 | if (root === null) {
45 | return;
46 | }
47 |
48 | let colorName: keyof ThemeColors;
49 | for (colorName in this.themeColors) {
50 | root.style.setProperty(`--${colorName}`, this.themeColors[colorName]);
51 | }
52 | }
53 |
54 | updateThemeColor(color: keyof ThemeColors, newVal: HexValue) {
55 | this.themeColors[color] = newVal;
56 | this.apply();
57 | }
58 |
59 | updateThemeColors(colors: Partial) {
60 | let colorName: keyof ThemeColors;
61 | for (colorName in colors) {
62 | const color = colors[colorName];
63 |
64 | if (color) {
65 | this.themeColors[colorName] = color;
66 | }
67 | }
68 |
69 | this.apply();
70 | }
71 |
72 | setTheme(from: string) {
73 | const newTheme = prepareThemeColors(from);
74 | this.themeColors = newTheme;
75 | this.srcColor = from;
76 | this.apply();
77 | }
78 | }
79 |
80 | export default new QTheme();
81 |
--------------------------------------------------------------------------------
/src/lib/components/avatar/QAvatar.scss:
--------------------------------------------------------------------------------
1 | @use "$css/variables";
2 |
3 | @layer q-avatar {
4 | // Basic styles
5 | .q-avatar {
6 | height: var(--size);
7 | width: var(--size);
8 |
9 | & > img,
10 | & > video {
11 | object-fit: cover;
12 | object-position: center;
13 | aspect-ratio: 1;
14 | width: 100%;
15 | height: 100%;
16 | }
17 | transition:
18 | variables.$speed3 transform,
19 | variables.$speed3 border-radius,
20 | variables.$speed3 padding;
21 | border-radius: 0;
22 |
23 | display: flex;
24 | align-items: center;
25 | justify-content: center;
26 |
27 | font-size: 1rem;
28 | text-transform: uppercase;
29 |
30 | overflow: hidden;
31 |
32 | -webkit-user-select: none;
33 | -moz-user-select: none;
34 | -ms-user-select: none;
35 | user-select: none;
36 |
37 | &.circle {
38 | margin: 0;
39 | }
40 | }
41 |
42 | // Sizes
43 | $sizes: (
44 | "xs": 0,
45 | "sm": 1,
46 | "md": 2,
47 | "lg": 3,
48 | "xl": 4,
49 | );
50 |
51 | .q-avatar {
52 | @each $size, $val in $sizes {
53 | &--#{$size} {
54 | --size: calc(2rem + #{$val} * 0.5rem);
55 | }
56 | }
57 | }
58 |
59 | // Border radius
60 | $positions-y: "top", "bottom";
61 | $positions-x: "right", "left";
62 |
63 | .q-avatar {
64 | border-radius: 50%;
65 |
66 | &--square {
67 | border-radius: 0;
68 | }
69 |
70 | &--top-right-round {
71 | border-bottom-left-radius: 0;
72 | }
73 | &--top-left-round {
74 | border-bottom-right-radius: 0;
75 | }
76 | &--bottom-right-round {
77 | border-top-left-radius: 0;
78 | }
79 | &--bottom-left-round {
80 | border-top-right-radius: 0;
81 | }
82 |
83 | @each $pos-y in $positions-y {
84 | &--#{$pos-y}-round {
85 | @extend .q-avatar--#{$pos-y}-left-round;
86 | @extend .q-avatar--#{$pos-y}-right-round;
87 | }
88 | }
89 |
90 | @each $pos-x in $positions-x {
91 | &--#{$pos-x}-round {
92 | @extend .q-avatar--top-#{$pos-x}-round;
93 | @extend .q-avatar--bottom-#{$pos-x}-round;
94 | }
95 | }
96 |
97 | &--round {
98 | border-radius: 0.5rem;
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/lib/components/index.ts:
--------------------------------------------------------------------------------
1 | import QAvatar from "./avatar/QAvatar.svelte";
2 | import QBreadcrumbs from "./breadcrumbs/QBreadcrumbs.svelte";
3 | import QBreadcrumbsEl from "./breadcrumbs/QBreadcrumbsEl.svelte";
4 | import QBtn from "./button/QBtn.svelte";
5 | import QCard from "./card/QCard.svelte";
6 | import QCardSection from "./card/QCardSection.svelte";
7 | import QCardActions from "./card/QCardActions.svelte";
8 | import QCheckbox from "./checkbox/QCheckbox.svelte";
9 | import QChip from "./chip/QChip.svelte";
10 | import QCircularProgress from "./progress/QCircularProgress.svelte";
11 | import QCodeBlock from "./codeBlock/QCodeBlock.svelte";
12 | import QDialog from "./dialog/QDialog.svelte";
13 | import QDrawer from "./drawer/QDrawer.svelte";
14 | import QExpansionItem from "./expansion-item/QExpansionItem.svelte";
15 | import QFooter from "./footer/QFooter.svelte";
16 | import QHeader from "./header/QHeader.svelte";
17 | import QIcon from "./icon/QIcon.svelte";
18 | import QInput from "./input/QInput.svelte";
19 | import QSelect from "./select/QSelect.svelte";
20 | import QLayout from "./layout/QLayout.svelte";
21 | import QList from "./list/QList.svelte";
22 | import QItem from "./list/QItem.svelte";
23 | import QItemSection from "./list/QItemSection.svelte";
24 | import QLinearProgress from "./progress/QLinearProgress.svelte";
25 | import QRadio from "./radio/QRadio.svelte";
26 | import QRailbar from "./railbar/QRailbar.svelte";
27 | import QSeparator from "./separator/QSeparator.svelte";
28 | import QTabs from "./tabs/QTabs.svelte";
29 | import QTab from "./tabs/QTab.svelte";
30 | import QTable from "./table/QTable.svelte";
31 | import QSwitch from "./switch/QSwitch.svelte";
32 | import QToolbar from "./toolbar/QToolbar.svelte";
33 | import QToolbarTitle from "./toolbar/QToolbarTitle.svelte";
34 | import QTooltip from "./tooltip/QTooltip.svelte";
35 |
36 | export {
37 | QAvatar,
38 | QBreadcrumbs,
39 | QBreadcrumbsEl,
40 | QBtn,
41 | QCard,
42 | QCardSection,
43 | QCardActions,
44 | QCheckbox,
45 | QChip,
46 | QCircularProgress,
47 | QCodeBlock,
48 | QDialog,
49 | QDrawer,
50 | QExpansionItem,
51 | QFooter,
52 | QHeader,
53 | QIcon,
54 | QInput,
55 | QSelect,
56 | QLayout,
57 | QList,
58 | QItem,
59 | QItemSection,
60 | QLinearProgress,
61 | QRadio,
62 | QRailbar,
63 | QSeparator,
64 | QTabs,
65 | QTab,
66 | QTable,
67 | QSwitch,
68 | QToolbar,
69 | QToolbarTitle,
70 | QTooltip,
71 | };
72 |
--------------------------------------------------------------------------------
/src/lib/components/drawer/props.ts:
--------------------------------------------------------------------------------
1 | import type { NativeProps } from "$utils";
2 | import type { HTMLAttributes } from "svelte/elements";
3 |
4 | export type QDrawerSideOptions = "left" | "right";
5 | export type QDrawerBehaviorOptions = "default" | "desktop" | "mobile";
6 |
7 | export interface QDrawerProps extends NativeProps, HTMLAttributes {
8 | /**
9 | * The value indicating whether the drawer is visible or hidden.
10 | * @default true
11 | */
12 | value?: boolean;
13 |
14 | /**
15 | * The side of the layout where the drawer is positioned.
16 | * @default "left"
17 | */
18 | side?: QDrawerSideOptions;
19 |
20 | /**
21 | * The width of the drawer. Can be specified with a CSS unit. If no unit is specified, "px" will be used.
22 | * @default 300
23 | */
24 | width?: number;
25 |
26 | /**
27 | * The breakpoint at which the drawer behavior changes. (not supported yet)
28 | * @default 1023
29 | */
30 | breakpoint?: number;
31 |
32 | /**
33 | * Determines whether the drawer should be shown if the viewport width is above the specified breakpoint. (not supported yet)
34 | * @default false
35 | */
36 | showIfAbove?: boolean;
37 |
38 | /**
39 | * The behavior of the drawer based on the viewport width. (not supported yet)
40 | * @default "default"
41 | */
42 | behavior?: QDrawerBehaviorOptions;
43 |
44 | /**
45 | * Determines whether the drawer has a border around it.
46 | * @default false
47 | */
48 | bordered?: boolean;
49 |
50 | /**
51 | * Determines whether the drawer has an elevated effect. (not supported yet)
52 | * @default false
53 | */
54 | elevated?: boolean;
55 |
56 | /**
57 | * Determines whether the wrawer should behave like an overlay (opening above the content) or not (pushing the content while opening).
58 | * @default false
59 | */
60 | overlay?: boolean;
61 |
62 | /**
63 | * Determines whether the drawer remains persistent, not closing on click outside.
64 | * @default false
65 | */
66 | persistent?: boolean;
67 |
68 | /**
69 | * Determines whether swipe gestures opening the drawer should be disabled or not.
70 | * @default false
71 | */
72 | noSwipe?: boolean;
73 |
74 | /**
75 | * The threshold in percentage of the drawer width that must be swiped for the drawer to snap open/close.
76 | * This is only applicable if swipe gestures are enabled.
77 | * @default "30%"
78 | */
79 | swipeThreshold?: `${number}%`;
80 | }
81 |
--------------------------------------------------------------------------------
/src/lib/components/breadcrumbs/QBreadcrumbsEl.svelte:
--------------------------------------------------------------------------------
1 |
35 |
36 | {#snippet fallback()}
37 | {label}
38 | {/snippet}
39 |
40 | {#snippet breadcrumbEl()}
41 | {#if icon !== undefined}
42 | {#if typeof icon === "string"}
43 |
44 | {:else}
45 |
46 | {@render icon()}
47 |
48 | {/if}
49 | {/if}
50 |
51 |
52 | {@render children()}
53 |
54 | {/snippet}
55 |
56 |
61 |
62 | {#if typeof ctx.separator === "string"}
63 | {#if ctx.separator.startsWith("icon:")}
64 |
65 | {:else}
66 | {ctx.separator}
67 | {/if}
68 | {:else}
69 | {@render ctx.separator?.()}
70 | {/if}
71 |
72 |
73 | {#if href || to}
74 |
80 | {@render breadcrumbEl()}
81 |
82 | {:else}
83 |
84 | {@render breadcrumbEl()}
85 |
86 | {/if}
87 |
88 |
--------------------------------------------------------------------------------
/src/lib/components/input/props.ts:
--------------------------------------------------------------------------------
1 | import type { NativeProps } from "$utils";
2 | import type { Snippet } from "svelte";
3 | import type { HTMLAttributes } from "svelte/elements";
4 |
5 | export interface QInputProps extends NativeProps, HTMLAttributes {
6 | /**
7 | * Makes the input component more compact.
8 | *
9 | * @default false
10 | */
11 | dense?: boolean;
12 |
13 | /**
14 | * Disables the input, preventing user interaction.
15 | *
16 | * @default false
17 | */
18 | disable?: boolean;
19 |
20 | /**
21 | * Indicates an error state for the input.
22 | *
23 | * @default false
24 | */
25 | error?: boolean;
26 |
27 | /**
28 | * Message to display when the input is in an error state. Overrides the hint prop when the input is in an error state.
29 | *
30 | * @default undefined
31 | */
32 | errorMessage?: string;
33 |
34 | /**
35 | * Applies a filled background style to the input.
36 | *
37 | * @default false
38 | */
39 | filled?: boolean;
40 |
41 | /**
42 | * Helper text displayed below the input. When the input is in an error state, the helper text will be overwritten by the error message.
43 | *
44 | * @default undefined
45 | */
46 | hint?: string;
47 |
48 | /**
49 | * Label text for the input field.
50 | *
51 | * @default undefined
52 | */
53 | label?: string;
54 |
55 | /**
56 | * Applies an outlined style to the input.
57 | *
58 | * @default false
59 | */
60 | outlined?: boolean;
61 |
62 | /**
63 | * Makes the sides of the input round.
64 | *
65 | * @default false
66 | */
67 | rounded?: boolean;
68 |
69 | /**
70 | * Current value of the input field. This property is bindable.
71 | */
72 | value: string;
73 |
74 | /**
75 | * Content to be placed before the input wrapper element, usually an icon.
76 | *
77 | * @default undefined
78 | */
79 | before?: Snippet;
80 |
81 | /**
82 | * Content to be placed at the start of the input field, usually an icon.
83 | *
84 | * @default undefined
85 | */
86 | prepend?: Snippet;
87 |
88 | /**
89 | * Content to be placed at the end of the input field, usually an icon.
90 | *
91 | * @default undefined
92 | */
93 | append?: Snippet;
94 |
95 | /**
96 | * Content to be placed after the input wrapper element, usually an icon.
97 | *
98 | * @default undefined
99 | */
100 | after?: Snippet;
101 | }
102 |
--------------------------------------------------------------------------------
/src/lib/components/separator/props.ts:
--------------------------------------------------------------------------------
1 | import type { HTMLAttributes } from "svelte/elements";
2 |
3 | export interface QSeparatorVerticalProps {
4 | /**
5 | * Spacing around the separator.
6 | *
7 | * @default "none"
8 | */
9 | spacing?: Q.Size;
10 |
11 | /**
12 | * Adds horizontal padding to the separator container, adding space around the separator.
13 | *
14 | * @default false
15 | */
16 | inset?: boolean;
17 |
18 | /**
19 | * Sets the separator orientation to vertical.
20 | *
21 | * @default undefined
22 | */
23 | vertical?: true;
24 |
25 | /**
26 | * Color of the separator line.
27 | *
28 | * @default "outline"
29 | */
30 | color?: string;
31 |
32 | /**
33 | * Custom size (thickness) of the separator line.
34 | *
35 | * @default undefined
36 | */
37 | size?: string;
38 |
39 | /**
40 | * Text to display. Its position on the separator is determined by the textAlign prop.
41 | *
42 | * @default undefined
43 | */
44 | text?: string;
45 |
46 | /**
47 | * Vertical alignment of the text when text prop is used.
48 | *
49 | * @default "middle"
50 | */
51 | textAlign?: "top" | "middle" | "bottom";
52 | }
53 |
54 | export interface QSeparatorHorizontalProps {
55 | /**
56 | * Spacing around the separator.
57 | *
58 | * @default "none"
59 | */
60 | spacing?: Q.Size;
61 |
62 | /**
63 | * Adds vertical padding to the separator container, adding space around the separator.
64 | *
65 | * @default false
66 | */
67 | inset?: boolean;
68 |
69 | /**
70 | * Sets the separator orientation to horizontal.
71 | *
72 | * @default undefined
73 | */
74 | vertical?: false;
75 |
76 | /**
77 | * Color of the separator line.
78 | *
79 | * @default "outline"
80 | */
81 | color?: string;
82 |
83 | /**
84 | * Custom size (thickness) of the separator line.
85 | *
86 | * @default undefined
87 | */
88 | size?: string;
89 |
90 | /**
91 | * Text to display. Its position on the separator is determined by the textAlign prop.
92 | *
93 | * @default undefined
94 | */
95 | text?: string;
96 |
97 | /**
98 | * Horizontal alignment of the text when text prop is used.
99 | *
100 | * @default "center"
101 | */
102 | textAlign?: "left" | "center" | "right";
103 | }
104 |
105 | export type QSeparatorProps = (QSeparatorHorizontalProps | QSeparatorVerticalProps) &
106 | HTMLAttributes;
107 |
--------------------------------------------------------------------------------
/src/lib/utils/context.ts:
--------------------------------------------------------------------------------
1 | import { getContext, hasContext, setContext } from "svelte";
2 |
3 | /**
4 | * This function allows to manipulate contexts more easily.
5 | * It avoids having to pass a Svelte store down the components but rather use runes to keep the context reactive.
6 | */
7 | export function QContext(name: string) {
8 | const sym = Symbol(name);
9 |
10 | return {
11 | /**
12 | * The inner symbol used to identify the context.
13 | */
14 | get symbol() {
15 | return sym;
16 | },
17 |
18 | /**
19 | * Get the context value.
20 | * @returns The context value or undefined if not found.
21 | */
22 | get() {
23 | return getContext(sym);
24 | },
25 |
26 | /**
27 | * Get the context value or throw an error if not found.
28 | *
29 | * @param errorMsg Optional error message to throw if context is not found.
30 | * @returns The context value.
31 | * @throws Error if context is not found.
32 | */
33 | assertGet(errorMsg?: string) {
34 | const ctx = getContext(sym);
35 |
36 | if (!ctx) {
37 | throw new Error(errorMsg || `Context "${name}" not found`);
38 | }
39 |
40 | return ctx;
41 | },
42 |
43 | /**
44 | * Set the context value.
45 | * @param context The context value to set.
46 | */
47 | set(context: T) {
48 | setContext(sym, context);
49 | },
50 |
51 | /**
52 | * Reset the context value.
53 | */
54 | reset() {
55 | setContext(sym, undefined);
56 | },
57 |
58 | /**
59 | * Checks whether the context exists.
60 | * @returns True if the context exists, false otherwise.
61 | */
62 | exists() {
63 | return hasContext(sym);
64 | },
65 |
66 | /**
67 | * Update one entry of the context.
68 | *
69 | * @param key The key of the entry to update.
70 | * @param value The new value for the entry.
71 | */
72 | updateEntry(key: keyof T, value: NonNullable[keyof T]) {
73 | const ctx = getContext(sym);
74 |
75 | if (!ctx) {
76 | return;
77 | }
78 |
79 | ctx[key] = value;
80 | },
81 |
82 | /**
83 | * Update multiple entries of the context.
84 | *
85 | * @param updates The key/value pairs to update in the context.
86 | */
87 | updateEntries(updates: Partial) {
88 | const ctx = getContext(sym);
89 |
90 | if (!ctx) {
91 | return;
92 | }
93 |
94 | Object.assign(ctx, updates);
95 | },
96 | };
97 | }
98 |
--------------------------------------------------------------------------------
/docgen/props/WorkerManager.ts:
--------------------------------------------------------------------------------
1 | import { Worker } from "worker_threads";
2 | import os from "os";
3 | import EventEmitter from "events";
4 |
5 | type ExtendedWorker = {
6 | worker: Worker;
7 | isFree: boolean;
8 | };
9 |
10 | export type WorkerTask = {
11 | propsFilePath: string;
12 | docsPropsFilePath: string;
13 | hashProps?: string;
14 | };
15 |
16 | export default class WorkerManager extends EventEmitter {
17 | private workerPath: string;
18 | private maxWorkers: number;
19 | private workers: ExtendedWorker[];
20 | private tasks: WorkerTask[];
21 | private types: Record;
22 |
23 | constructor(workerPath: string, tasks: WorkerTask[]) {
24 | super();
25 | this.workerPath = workerPath;
26 | const availableWorkers = os.availableParallelism();
27 | this.maxWorkers = availableWorkers >= 6 ? availableWorkers - 2 : availableWorkers - 1;
28 | this.tasks = tasks;
29 | this.workers = this.initializeWorkers();
30 | this.types = {};
31 | }
32 |
33 | get hasActiveTasks() {
34 | return this.workers.some((worker) => !worker.isFree);
35 | }
36 |
37 | private initializeWorkers(): ExtendedWorker[] {
38 | const workers = [];
39 | const workerCount = Math.min(this.maxWorkers, this.tasks.length);
40 |
41 | for (let i = 0; i < workerCount; i++) {
42 | const worker = new Worker(this.workerPath);
43 | const extWorker: ExtendedWorker = {
44 | worker,
45 | isFree: true,
46 | };
47 |
48 | worker.on("message", (message) => {
49 | if (message.event === "finished") {
50 | this.addToTypes(message.types);
51 | extWorker.isFree = true;
52 |
53 | this.assignNextTask(extWorker);
54 |
55 | this.checkIfFinished();
56 | }
57 | });
58 |
59 | workers.push(extWorker);
60 | this.assignNextTask(extWorker);
61 | }
62 |
63 | return workers;
64 | }
65 |
66 | private checkIfFinished() {
67 | if (!this.hasActiveTasks) {
68 | this.emit("finished", this.types);
69 | this.workers.forEach((worker) => worker.worker.terminate());
70 | }
71 | }
72 |
73 | private addToTypes(newTypes: Record) {
74 | Object.assign(this.types, newTypes);
75 | }
76 |
77 | private assignTaskToWorker(worker: ExtendedWorker, task: WorkerTask) {
78 | worker.isFree = false;
79 | worker.worker.postMessage(task);
80 | }
81 |
82 | private assignNextTask(worker: ExtendedWorker) {
83 | if (this.tasks.length > 0) {
84 | const nextTask = this.tasks.shift();
85 | this.assignTaskToWorker(worker, nextTask!);
86 | } else {
87 | worker.worker.terminate();
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/app.d.ts:
--------------------------------------------------------------------------------
1 | // See https://kit.svelte.dev/docs/types#app
2 | // for information about these interfaces
3 | declare global {
4 | namespace App {
5 | // interface Error {}
6 | // interface Locals {}
7 | // interface PageData {}
8 | // interface Platform {}
9 | }
10 |
11 | namespace Q {
12 | export type Size = "none" | "xs" | "sm" | "md" | "lg" | "xl";
13 |
14 | export type CssUnit = "px" | "%" | "em" | "ex" | "ch" | "rem" | "vw" | "vh" | "vmin" | "vmax";
15 |
16 | export type CssValue = `${number}${CssUnit}`;
17 |
18 | export type StringWithSuggestions = T | (string & {});
19 |
20 | export interface QComponentDocs {
21 | name: string;
22 | description: string;
23 | docs: {
24 | props: QComponentProp[];
25 | slots: QComponentSlot[];
26 | methods: QComponentMethod[];
27 | events: QComponentEvent[];
28 | };
29 | }
30 |
31 | export interface QComponentProp {
32 | name: string;
33 | type: string;
34 | default?: unknown;
35 | description: string;
36 | clickableType?: boolean;
37 | optional?: boolean;
38 | }
39 |
40 | export interface QComponentSlot {
41 | name: string;
42 | description: string;
43 | }
44 |
45 | export interface QComponentType {
46 | name: string;
47 | description: string;
48 | }
49 |
50 | export interface QComponentEvent {
51 | name: string;
52 | type: string;
53 | description: string;
54 | }
55 |
56 | export interface QComponentMethod {
57 | name: string;
58 | type: string;
59 | description: string;
60 | }
61 |
62 | type ComponentName = `q-${string}`;
63 |
64 | interface QuaffClassesParams {
65 | bemClasses?: Record;
66 | classes?: unknown[];
67 | isCustomComponent?: boolean;
68 | }
69 |
70 | /**
71 | * Function that allows a preprocessor to add static and dynamic classes on the chosen component (identified by `componentName`).
72 | *
73 | * @param componentName - Main class of the target element. The element should only have this class to be well targeted. If you need to add other classes, use this function's `classes` parameter.
74 | * @param bemClasses - Classes that should be prefixed with `componentName`
75 | * @param classes - Classes that should not be prefixed with `componentName`. Use arrow function if Svelte cries about `$derived` values
76 | */
77 | export function classes(
78 | componentName: string,
79 | { bemClasses, classes, isCustomComponent }: QuaffClassesParams
80 | ): {
81 | class: string;
82 | };
83 | }
84 | }
85 |
86 | export {};
87 |
--------------------------------------------------------------------------------
/src/routes/+page.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 | Quaff – Svelte Material 3 UI Framework
17 |
18 |
19 |
20 |
21 |
26 |
27 |
Quaff is a UI Component Framework built on top of SvelteKit.
28 |
29 |
30 | npm create quaff@latest
31 |
37 |
38 |
39 | Check our components and familiarize yourself with our
40 | grid , utils and the rest of our docs.
41 |
42 |
43 | Found an issue? Rage Comment on Quaff's
44 | GitHub page .
45 |
46 |
47 |
48 |
49 |
103 |
--------------------------------------------------------------------------------
/src/lib/components/progress/QLinearProgress.svelte:
--------------------------------------------------------------------------------
1 |
55 |
56 |
83 |
--------------------------------------------------------------------------------
/src/routes/utils/+page.svelte:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 | {pageTitle("Utils")}
32 |
33 |
34 |
35 |
36 | {#each utils as util (util.name)}
37 |
(util.hovered = true)}
43 | onmouseleave={() => (util.hovered = false)}
44 | onclick={() => goto(util.href)}
45 | onkeypress={(e) => {
46 | if (e.key === "Enter") {
47 | goto(util.href);
48 | }
49 | }}
50 | >
51 |
56 |
57 | {util.name}
58 |
59 |
60 |
61 | {util.description}
62 |
63 |
64 |
65 | {/each}
66 |
67 |
68 |
69 |
89 |
--------------------------------------------------------------------------------
/src/routes/components/layout/docs.snippets.ts:
--------------------------------------------------------------------------------
1 | import { capitalize } from "$utils";
2 | import type { QLayoutProps } from "$components/layout/props";
3 |
4 | function drawerBtn (side: "left" | "right") {
5 | return ` (${side}DrawerShown = !${side}DrawerShown)} />`
6 | }
7 |
8 | function hfBuilder(
9 | kind: "header" | "footer",
10 | leftDrawer: boolean,
11 | rightDrawer: boolean
12 | ) {
13 | const classes = (!leftDrawer || rightDrawer) ? ' class="justify-between"' : ""
14 |
15 | const snippetTemplate = [
16 | ` {#snippet ${kind}()}`,
17 | ` `,
18 | leftDrawer && drawerBtn("left"),
19 | ` Header
`,
20 | rightDrawer && drawerBtn("right"),
21 | ` `,
22 | ` {/snippet}${kind === "header" ? "\n" : ""}`,
23 | ]
24 |
25 | return snippetTemplate.filter(Boolean).join("\n")
26 | }
27 |
28 | function navbarBuilder(kind: "railbar" | "drawer", side: "left" | "right") {
29 | const attributes = [
30 | "bordered",
31 | ]
32 |
33 | if (kind === "drawer") {
34 | attributes.push("persistent", `bind:value={${side}DrawerShown}`)
35 | }
36 |
37 | return [
38 | ` {#snippet ${kind}${capitalize(side)}()}`,
39 | ` `,
40 | ` `,
41 | ` `,
42 | ` `,
43 | ` Home `,
44 | ` `,
45 | ` `,
46 | ` `,
47 | ` About `,
48 | ` `,
49 | ` `,
50 | ` `,
51 | ` Store `,
52 | ` `,
53 | ` `,
54 | ` `,
55 | ` Contact `,
56 | ` `,
57 | ` `,
58 | ` `,
59 | ` {/snippet}\n`,
60 | ].join("\n")
61 | }
62 |
63 | export const snippet = (
64 | view: QLayoutProps["view"],
65 | [header, footer, leftRailbar, leftDrawer, rightRailbar, rightDrawer]: boolean[]
66 | ) => {
67 | const result = [
68 | ``,
69 | header && hfBuilder("header", leftDrawer, rightDrawer),
70 | leftRailbar && navbarBuilder("railbar", "left"),
71 | rightRailbar && navbarBuilder("railbar", "right"),
72 | leftDrawer && navbarBuilder("drawer", "left"),
73 | rightDrawer && navbarBuilder("drawer", "right"),
74 | footer && hfBuilder("footer", leftDrawer, rightDrawer),
75 | ` `,
76 | ]
77 |
78 | return {
79 | "Trying different layouts": result.filter(Boolean).join("\n"),
80 | };
81 | };
82 |
--------------------------------------------------------------------------------