diff --git a/.storybook/inertia-mock.ts b/.storybook/inertia-mock.ts new file mode 100644 index 00000000000..722291a9bd2 --- /dev/null +++ b/.storybook/inertia-mock.ts @@ -0,0 +1,437 @@ +import {type App, defineComponent, h, type PropType, reactive} from 'vue'; + +/** + * Default page props for Storybook + * These can be overridden per-story using parameters.inertia + */ +export const defaultPageProps = { + craft: { + system: { + name: 'Craft CMS', + icon: null, + }, + app: { + version: '6.0.0', + edition: { + name: 'Pro' as const, + handle: 'pro' as const, + value: 2 as const, + }, + }, + site: { + url: 'https://example.com', + }, + currentUser: { + id: 1, + username: 'admin', + email: 'admin@example.com', + admin: true, + }, + nav: [], + actionUrl: '/actions/', + cpUrl: '/admin/', + baseApiUrl: '/api/', + }, + flash: { + success: null, + error: null, + }, + readOnly: false, +}; + +export type PageProps = typeof defaultPageProps & Record; + +/** + * Reactive page state that can be modified by stories + */ +export const pageState = reactive({ + props: {...defaultPageProps} as PageProps, + url: '/', + component: 'Story', + version: '1', + scrollRegions: [], + rememberedState: {}, + clearHistory: false, + encryptHistory: false, +}); + +/** + * Reset page props to defaults, optionally merging with overrides + */ +export function setPageProps(overrides: Partial = {}) { + pageState.props = deepMerge({...defaultPageProps}, overrides) as PageProps; +} + +/** + * Deep merge utility + */ +function deepMerge>( + target: T, + source: Partial +): T { + const result = {...target}; + + for (const key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + const sourceValue = source[key]; + const targetValue = result[key]; + + if ( + sourceValue && + typeof sourceValue === 'object' && + !Array.isArray(sourceValue) && + targetValue && + typeof targetValue === 'object' && + !Array.isArray(targetValue) + ) { + result[key] = deepMerge( + targetValue as Record, + sourceValue as Record + ) as T[Extract]; + } else { + result[key] = sourceValue as T[Extract]; + } + } + } + + return result; +} + +/** + * Mock usePage composable + */ +export function usePage() { + return pageState as unknown as { + props: T; + url: string; + component: string; + version: string; + }; +} + +/** + * Mock router for Inertia + */ +export const router = { + visit: (url: string, options?: Record) => { + console.log('[Storybook] router.visit:', url, options); + }, + get: ( + url: string, + data?: Record, + options?: Record + ) => { + console.log('[Storybook] router.get:', url, data, options); + }, + post: ( + url: string, + data?: Record, + options?: Record + ) => { + console.log('[Storybook] router.post:', url, data, options); + }, + put: ( + url: string, + data?: Record, + options?: Record + ) => { + console.log('[Storybook] router.put:', url, data, options); + }, + patch: ( + url: string, + data?: Record, + options?: Record + ) => { + console.log('[Storybook] router.patch:', url, data, options); + }, + delete: (url: string, options?: Record) => { + console.log('[Storybook] router.delete:', url, options); + }, + reload: (options?: Record) => { + console.log('[Storybook] router.reload:', options); + }, + replace: (url: string, options?: Record) => { + console.log('[Storybook] router.replace:', url, options); + }, + on: (event: string, callback: (...args: unknown[]) => void) => { + console.log('[Storybook] router.on:', event); + return () => {}; + }, +}; + +/** + * Mock useForm composable + */ +export interface InertiaForm> { + data(): TForm; + transform(callback: (data: TForm) => Record): this; + defaults(): this; + defaults(field: keyof TForm, value: unknown): this; + defaults(fields: Partial): this; + reset(...fields: (keyof TForm)[]): this; + clearErrors(...fields: (keyof TForm)[]): this; + setError(field: keyof TForm, value: string): this; + setError(errors: Record): this; + submit(method: string, url: string, options?: Record): void; + get(url: string, options?: Record): void; + post(url: string, options?: Record): void; + put(url: string, options?: Record): void; + patch(url: string, options?: Record): void; + delete(url: string, options?: Record): void; + cancel(): void; + errors: Partial>; + hasErrors: boolean; + processing: boolean; + progress: {percentage: number} | null; + wasSuccessful: boolean; + recentlySuccessful: boolean; + isDirty: boolean; +} + +export function useForm>( + initialData: TForm +): InertiaForm & TForm; +export function useForm>( + rememberKey: string, + initialData: TForm +): InertiaForm & TForm; +export function useForm>( + rememberKeyOrData: string | TForm, + maybeData?: TForm +): InertiaForm & TForm { + const data = + typeof rememberKeyOrData === 'string' ? maybeData! : rememberKeyOrData; + const formData = reactive({...data}) as TForm; + + const form = reactive({ + ...formData, + errors: {} as Partial>, + hasErrors: false, + processing: false, + progress: null as {percentage: number} | null, + wasSuccessful: false, + recentlySuccessful: false, + isDirty: false, + data() { + return formData; + }, + transform(callback: (data: TForm) => Record) { + return this; + }, + defaults(...args: unknown[]) { + return this; + }, + reset(...fields: (keyof TForm)[]) { + if (fields.length === 0) { + Object.assign(formData, data); + } else { + fields.forEach((field) => { + (formData as Record)[field as string] = data[field]; + }); + } + return this; + }, + clearErrors(...fields: (keyof TForm)[]) { + if (fields.length === 0) { + this.errors = {}; + } else { + fields.forEach((field) => { + delete this.errors[field]; + }); + } + this.hasErrors = Object.keys(this.errors).length > 0; + return this; + }, + setError( + fieldOrErrors: keyof TForm | Record, + maybeValue?: string + ) { + if (typeof fieldOrErrors === 'string') { + this.errors[fieldOrErrors] = maybeValue; + } else { + Object.assign(this.errors, fieldOrErrors); + } + this.hasErrors = true; + return this; + }, + submit(method: string, url: string, options?: Record) { + console.log('[Storybook] form.submit:', method, url, formData, options); + }, + get(url: string, options?: Record) { + this.submit('get', url, options); + }, + post(url: string, options?: Record) { + this.submit('post', url, options); + }, + put(url: string, options?: Record) { + this.submit('put', url, options); + }, + patch(url: string, options?: Record) { + this.submit('patch', url, options); + }, + delete(url: string, options?: Record) { + this.submit('delete', url, options); + }, + cancel() { + console.log('[Storybook] form.cancel'); + }, + }) as InertiaForm & TForm; + + return form; +} + +/** + * Mock Head component + */ +export const Head = defineComponent({ + name: 'Head', + props: { + title: String, + }, + setup(props, {slots}) { + // In Storybook, we just render nothing for Head + return () => null; + }, +}); + +/** + * Mock Link component + */ +export interface InertiaLinkProps { + href: string; + method?: 'get' | 'post' | 'put' | 'patch' | 'delete'; + data?: Record; + replace?: boolean; + preserveScroll?: boolean; + preserveState?: boolean; + only?: string[]; + headers?: Record; + as?: string; +} + +export const Link = defineComponent({ + name: 'Link', + props: { + href: { + type: String, + required: true, + }, + method: { + type: String as PropType<'get' | 'post' | 'put' | 'patch' | 'delete'>, + default: 'get', + }, + data: Object as PropType>, + replace: Boolean, + preserveScroll: Boolean, + preserveState: Boolean, + only: Array as PropType, + headers: Object as PropType>, + as: { + type: String, + default: 'a', + }, + }, + setup(props, {slots, attrs}) { + return () => + h( + props.as, + { + ...attrs, + href: props.href, + onClick: (e: Event) => { + e.preventDefault(); + console.log( + '[Storybook] Link clicked:', + props.href, + props.method, + props.data + ); + }, + }, + slots.default?.() + ); + }, +}); + +/** + * Mock Form component + */ +export const Form = defineComponent({ + name: 'Form', + props: { + method: { + type: String as PropType<'get' | 'post' | 'put' | 'patch' | 'delete'>, + default: 'post', + }, + action: String, + data: Object as PropType>, + preserveScroll: Boolean, + preserveState: Boolean, + only: Array as PropType, + headers: Object as PropType>, + }, + setup(props, {slots, attrs}) { + return () => + h( + 'form', + { + ...attrs, + action: props.action, + method: props.method === 'get' ? 'get' : 'post', + onSubmit: (e: Event) => { + e.preventDefault(); + console.log( + '[Storybook] Form submitted:', + props.action, + props.method, + props.data + ); + }, + }, + slots.default?.() + ); + }, +}); + +/** + * Mock Deferred component - renders children immediately in Storybook + */ +export const Deferred = defineComponent({ + name: 'Deferred', + props: { + data: { + type: [String, Array] as PropType, + required: true, + }, + }, + setup(props, {slots}) { + // In Storybook, render children immediately (no deferred loading) + return () => slots.default?.(); + }, +}); + +/** + * Mock createInertiaApp - not typically used in Storybook stories + */ +export function createInertiaApp(options: Record) { + console.log('[Storybook] createInertiaApp called - this is a mock'); + return Promise.resolve(); +} + +/** + * Install the Inertia mock as a Vue plugin + */ +export function installInertiaMock(app: App) { + // Provide the page object for usePage + app.config.globalProperties.$page = pageState; + app.config.globalProperties.$inertia = router; + + // Register global components + app.component('Head', Head); + app.component('Link', Link); + app.component('InertiaLink', Link); + + // Also provide via provide/inject for composition API + app.provide('$inertia', router); + app.provide('$page', pageState); +} diff --git a/.storybook/main.ts b/.storybook/main.ts new file mode 100644 index 00000000000..5a5d40c81f7 --- /dev/null +++ b/.storybook/main.ts @@ -0,0 +1,67 @@ +import type {StorybookConfig} from '@storybook/vue3-vite'; +import {dirname, join} from 'path'; +import vue from '@vitejs/plugin-vue'; +import tailwindcss from '@tailwindcss/vite'; + +/** + * This function is used to resolve the absolute path of a package. + * It is needed in projects that use Yarn PnP or are set up within a monorepo. + */ +function getAbsolutePath(value: string): string { + return dirname(require.resolve(join(value, 'package.json'))); +} + +const config: StorybookConfig = { + stories: ['../resources/js/**/*.mdx', '../resources/js/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + addons: [ + getAbsolutePath('@storybook/addon-themes'), + getAbsolutePath('@storybook/addon-docs'), + getAbsolutePath('@storybook/addon-a11y'), + ], + framework: { + name: getAbsolutePath('@storybook/vue3-vite') as '@storybook/vue3-vite', + options: { + docgen: 'vue-component-meta', + }, + }, + viteFinal(config) { + // Storybook's vue3-vite framework adds its own Vue plugin with default options. + // We need to configure `isCustomElement` so Vue treats `craft-*` tags as web + // components (from @craftcms/cp) rather than trying to resolve them as Vue + // components. Since Vite's mergeConfig doesn't deep-merge plugin options, + // we remove Storybook's Vue plugin and add our own with the correct config. + const filteredPlugins = (config.plugins || []).flat().filter((plugin) => { + if (plugin && typeof plugin === 'object' && 'name' in plugin) { + return plugin.name !== 'vite:vue'; + } + return true; + }); + + return { + ...config, + plugins: [ + ...filteredPlugins, + tailwindcss(), + vue({ + template: { + compilerOptions: { + isCustomElement: (tag) => tag.startsWith('craft-'), + }, + }, + }), + ], + resolve: { + ...config.resolve, + alias: { + ...(config.resolve?.alias || {}), + '@': join(__dirname, '../resources/js'), + vue: 'vue/dist/vue.esm-bundler.js', + // Mock Inertia for Storybook + '@inertiajs/vue3': join(__dirname, 'inertia-mock.ts'), + }, + }, + }; + }, +}; + +export default config; diff --git a/.storybook/preview.css b/.storybook/preview.css new file mode 100644 index 00000000000..1664f9024a0 --- /dev/null +++ b/.storybook/preview.css @@ -0,0 +1,57 @@ +/* Storybook preview utilities */ + +/* Layout helpers for stories */ +.sb-flex { + display: flex; +} + +.sb-flex-col { + flex-direction: column; +} + +.sb-items-center { + align-items: center; +} + +.sb-justify-center { + justify-content: center; +} + +.sb-gap-2 { + gap: 0.5rem; +} + +.sb-gap-4 { + gap: 1rem; +} + +.sb-p-4 { + padding: 1rem; +} + +/* Grid utilities */ +.sb-grid { + display: grid; +} + +.sb-grid-cols-2 { + grid-template-columns: repeat(2, 1fr); +} + +.sb-grid-cols-3 { + grid-template-columns: repeat(3, 1fr); +} + +/* Stack layout */ +.sb-stack { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.sb-inline { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: center; +} diff --git a/.storybook/preview.ts b/.storybook/preview.ts new file mode 100644 index 00000000000..55285f71059 --- /dev/null +++ b/.storybook/preview.ts @@ -0,0 +1,62 @@ +import type {Preview} from '@storybook/vue3'; +import {setup} from '@storybook/vue3'; +import {withThemeByDataAttribute} from '@storybook/addon-themes'; +import '@craftcms/cp'; +import '../resources/css/cp.css'; +import './preview.css'; +import {installInertiaMock, type PageProps, setPageProps} from './inertia-mock'; + +// Install the Inertia mock globally +setup((app) => { + installInertiaMock(app); +}); + +// Declare module augmentation for Storybook parameters +declare module '@storybook/vue3' { + interface Parameters { + inertia?: Partial; + } +} + +const preview: Preview = { + parameters: { + controls: { + expanded: true, + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + options: { + storySort: { + method: 'alphabetical', + }, + }, + a11y: { + // 'todo' - show a11y violations in the test UI only + // 'error' - fail CI on a11y violations + // 'off' - skip a11y checks entirely + test: 'todo', + }, + }, + decorators: [ + // Inertia page props decorator - must come before theme decorator + (story, context) => { + // Reset and apply any story-specific page props + const inertiaProps = context.parameters.inertia || {}; + setPageProps(inertiaProps); + return story(); + }, + withThemeByDataAttribute({ + themes: { + light: 'light', + dark: 'dark', + }, + defaultTheme: 'light', + attributeName: 'data-theme', + }), + ], + tags: ['autodocs'], +}; + +export default preview; diff --git a/docs/admin-table.md b/docs/admin-table.md new file mode 100644 index 00000000000..ee2ab4b596b --- /dev/null +++ b/docs/admin-table.md @@ -0,0 +1,364 @@ +# AdminTable Component + +The `AdminTable` component renders data tables in the Craft CMS Control Panel. It wraps [TanStack Table (Vue)](https://tanstack.com/table/latest) with Craft's styling, accessibility, pagination, sorting, reordering, and empty-state handling. + +## Basic Usage + +```vue + + + +``` + +## Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `table` | TanStack `Table` instance | *required* | The table instance created by `useVueTable()`. | +| `title` | `string` | — | Table caption for screen readers (prefixed to sort instructions). | +| `reorderable` | `boolean` | `false` | Enables drag-and-drop row reordering with a drag handle column. | +| `selectable` | `boolean` | `true` | Reserved for future row selection support. | +| `readOnly` | `boolean` | — | When `true`, hides reorder handles (used with `reorderable`). | +| `layout` | `'auto' \| 'fixed'` | `'auto'` | CSS table layout mode. | +| `spacing` | `TableSpacingValue` | — | Row density: `'compact'`, `'relaxed'`, or `'spacious'`. | +| `from` | `number` | — | Start index of displayed rows (for "X–Y of Z" display). | +| `to` | `number` | — | End index of displayed rows. | +| `total` | `number` | — | Total number of items (all pages). | +| `enableAdjustPageSize` | `boolean` | `false` | Shows a "per page" dropdown in the footer. | +| `pageSizeOptions` | `number[]` | `[50, 100, 250]` | Options for the page-size dropdown. | + +## Events + +| Event | Payload | Description | +|-------|---------|-------------| +| `reorder` | `(startIndex: number, finishIndex: number)` | Emitted when a row is reordered via drag-and-drop or the keyboard buttons. | + +## Slots + +### `search-form` + +Renders above the table header. Use with the `SearchForm` component for server-side search: + +```vue + + + +``` + +### `empty-row` + +Custom content shown when the table has no rows. Falls back to a generic "No results" message. + +```vue + + + +``` + +## Column Meta Options + +TanStack Table's `meta` object on column definitions is used by `AdminTable` to control rendering behavior: + +| Meta Key | Type | Description | +|----------|------|-------------| +| `trackSize` | `string` | CSS grid track size for the column (e.g., `'1.5fr'`, `'34px'`, `'60px'`). Defaults to `1fr`. | +| `headerSrOnly` | `boolean` | Visually hides the header text (still available to screen readers). | +| `headerTip` | `string` | Displays an info icon tooltip next to the header. | +| `columnClass` | `string \| object` | CSS classes applied to both header and body cells. | +| `headerClass` | `string \| object` | CSS classes applied only to header cells. | +| `cellClass` | `string \| object` | CSS classes applied only to body cells. | +| `cellTag` | `string` | Override the cell HTML element (defaults to `'td'`). | +| `wrap` | `boolean` | Enables text wrapping in cells (cells are `nowrap` by default). | + +Example: + +```ts +columnHelper.accessor('searchable', { + header: t('Searchable'), + meta: { + trackSize: '34px', + headerSrOnly: true, + }, + enableSorting: false, + cell: ({row}) => { + if (row.original.searchable) { + return h('craft-icon', { + appearance: 'badge', + name: 'magnifying-glass', + label: t('Searchable'), + }); + } + }, +}); +``` + +--- + +## `createCraftColumnHelper` + +The `createCraftColumnHelper()` factory extends TanStack's `createColumnHelper` with Craft-specific column presets for common cell types. It returns a `CraftColumnHelper` that includes all the standard TanStack methods (`accessor`, `display`, `group`) plus four additional helpers: + +### `columnHelper.link(accessor, config?)` + +Renders the cell value as a bold `CpLink`. Use for the primary name/title column. + +```ts +columnHelper.link('name', { + header: t('Name'), + props: ({row}) => ({ + href: `/admin/things/${row.original.id}/edit`, + inertia: false, // use a plain tag (set true for Inertia navigation) + }), +}); +``` + +The `props` function receives the cell context and should return props for the `CpLink` component (e.g., `href`, `inertia`, `variant`). + +### `columnHelper.handle(accessor, config?)` + +Renders the cell value inside a `` web component, showing the handle with a click-to-copy button. Automatically sets the header to "Handle". + +```ts +columnHelper.handle('handle'); + +// With a custom header: +columnHelper.handle('handle', {header: t('API Handle')}); +``` + +### `columnHelper.date(accessor, config?)` + +Renders date values using the `Date` component, which formats them according to the user's locale. Handles both raw date strings and objects with a `.date` property. Displays "Never" when the value is empty. + +```ts +columnHelper.date('lastUsed', { + header: t('Last Used'), +}); + +columnHelper.date('expiryDate', { + header: t('Expires'), +}); +``` + +### `columnHelper.actions(actionsFn, config?)` + +Creates a display column (id: `'actions'`) for row action buttons. The header is set to "Actions" and visually hidden (screen-reader only). Actions are rendered in a right-aligned flex container. + +```ts +columnHelper.actions(({row}) => [ + h(DeleteButton, {onClick: () => deleteItem(row.original)}), +]); +``` + +The first argument is a function receiving the cell context and returning an array of VNodes (typically buttons). You can render any combination of components: + +```ts +columnHelper.actions(({row}) => [ + h(CpLink, {href: editUrl(row.original), appearance: 'button', size: 'small'}, () => t('Edit')), + h(DeleteButton, {onClick: () => handleDelete(row.original)}), +]); +``` + +### Using `accessor` and `display` directly + +The standard TanStack helpers are still available for columns that don't fit the presets: + +```ts +// Simple text column — just renders the value +columnHelper.accessor('type', { + header: t('Type'), +}); + +// Custom cell rendering with accessor +columnHelper.accessor('type', { + header: t('Type'), + cell: ({row, getValue}) => { + if (row.original.missing) { + return h('span', {class: 'c-color-error'}, getValue()); + } + return getValue(); + }, +}); + +// Display column (no data accessor) +columnHelper.display({ + id: 'type', + header: t('Type'), + cell: ({row}) => h('div', {class: 'flex items-center gap-2'}, [ + h('craft-icon', row.original.type.icon), + h('span', row.original.type.label), + ]), +}); +``` + +--- + +## Column Visibility + +Control which columns are shown using TanStack's `columnVisibility` state. This is useful for hiding the actions column when the user is in read-only mode: + +```ts +const table = useVueTable({ + data: props.data, + columns, + state: { + get columnVisibility() { + return { + name: true, + handle: true, + actions: !props.readOnly, + }; + }, + }, + getCoreRowModel: getCoreRowModel(), +}); +``` + +## Reorderable Rows + +Enable drag-and-drop reordering by setting `:reorderable="true"` and handling the `@reorder` event. The component adds a drag handle column and keyboard-accessible up/down buttons. + +```vue + + + +``` + +## Server-Side Pagination & Sorting + +For paginated data, use the `useServerPagination` and `useServerSort` composables and pass the pagination display props: + +```vue + + + +``` + +## Supporting Components + +| Component | Location | Description | +|-----------|----------|-------------| +| `SearchForm` | `@/components/AdminTable/SearchForm.vue` | Debounced search input with Inertia form submission. | +| `DeleteButton` | `@/components/AdminTable/DeleteButton.vue` | Small danger button with an "×" icon for row deletion. | +| `CpLink` | `@/components/CpLink.vue` | Link component supporting both Inertia and plain `` navigation. | +| `Empty` | `@/components/Empty.vue` | Empty state display with icon and optional action slot. | diff --git a/package-lock.json b/package-lock.json index 8cd3bbd26d7..d6122a564a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,10 @@ "@craftcms/webpack": "file:packages/craftcms-webpack", "@laravel/vite-plugin-wayfinder": "^0.1.7", "@playwright/test": "^1.58.2", + "@storybook/addon-a11y": "^9.1.5", + "@storybook/addon-docs": "^9.1.5", + "@storybook/addon-themes": "^9.1.5", + "@storybook/vue3-vite": "^9.1.5", "@tailwindcss/vite": "^4.2.2", "@total-typescript/tsconfig": "^1.0.4", "@vitejs/plugin-vue": "^6.0.5", @@ -36,6 +40,7 @@ "lint-staged": "^16.4.0", "npm-run-all": "^4.1.5", "prettier": "3.8.1", + "storybook": "^9.1.5", "stylelint": "^17.6.0", "stylelint-config-standard": "^40.0.0", "stylelint-config-standard-scss": "^17.0.0", @@ -2138,6 +2143,7 @@ }, "node_modules/@emnapi/core": { "version": "1.7.1", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2147,6 +2153,7 @@ }, "node_modules/@emnapi/runtime": { "version": "1.7.1", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2155,6 +2162,7 @@ }, "node_modules/@emnapi/wasi-threads": { "version": "1.1.0", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2168,12 +2176,12 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "aix" ], - "peer": true, "engines": { "node": ">=18" } @@ -2185,12 +2193,12 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -2202,12 +2210,12 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -2219,12 +2227,12 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -2236,12 +2244,12 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } @@ -2253,12 +2261,12 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } @@ -2270,12 +2278,12 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -2287,12 +2295,12 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -2304,12 +2312,12 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2321,12 +2329,12 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2338,12 +2346,12 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2355,12 +2363,12 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2372,12 +2380,12 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2389,12 +2397,12 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2406,12 +2414,12 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2423,12 +2431,12 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2440,12 +2448,12 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2457,12 +2465,12 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -2474,12 +2482,12 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -2491,12 +2499,12 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -2508,12 +2516,12 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -2525,12 +2533,12 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "openharmony" ], - "peer": true, "engines": { "node": ">=18" } @@ -2542,12 +2550,12 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "sunos" ], - "peer": true, "engines": { "node": ">=18" } @@ -2559,12 +2567,12 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -2576,12 +2584,12 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -2593,12 +2601,12 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -3406,6 +3414,7 @@ }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.1", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -4342,6 +4351,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4373,6 +4383,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4389,6 +4400,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4405,6 +4417,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4421,6 +4434,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4437,6 +4451,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4453,6 +4468,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4469,6 +4485,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4485,6 +4502,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4501,6 +4519,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4517,6 +4536,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4533,6 +4553,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -4549,6 +4570,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4582,6 +4604,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5050,6 +5073,119 @@ "storybook": "^9.1.16" } }, + "node_modules/@storybook/vue3": { + "version": "9.1.20", + "resolved": "https://registry.npmjs.org/@storybook/vue3/-/vue3-9.1.20.tgz", + "integrity": "sha512-hWuxUNq+ejoMQOTyIaVbyTSnPlEdWdbOOuM4QUCtmFLES7FIk1ZCamsncSY5jbWmnvD4anRQVy8Lb0XyBmQNhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "type-fest": "~2.19", + "vue-component-type-helpers": "latest" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^9.1.20", + "vue": "^3.0.0" + } + }, + "node_modules/@storybook/vue3-vite": { + "version": "9.1.20", + "resolved": "https://registry.npmjs.org/@storybook/vue3-vite/-/vue3-vite-9.1.20.tgz", + "integrity": "sha512-eOf0fLCqsUcnOe4XtwEy3TqCUMEHSNtJed5t+OcBK0asISABX0brQLquXLZdnVOcixzgayvGTpDTZLoNgzL1mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/builder-vite": "9.1.20", + "@storybook/vue3": "9.1.20", + "find-package-json": "^1.2.0", + "magic-string": "^0.30.0", + "typescript": "^5.8.3", + "vue-component-meta": "^2.0.0", + "vue-docgen-api": "^4.75.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^9.1.20", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@storybook/vue3-vite/node_modules/@storybook/builder-vite": { + "version": "9.1.20", + "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-9.1.20.tgz", + "integrity": "sha512-cdU3Q2/wEaT8h+mApFToRiF/0hYKH1eAkD0scQn67aODgp7xnkr0YHcdA+8w0Uxd2V7U8crV/cmT/HD0ELVOGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/csf-plugin": "9.1.20", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^9.1.20", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@storybook/vue3-vite/node_modules/@storybook/csf-plugin": { + "version": "9.1.20", + "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-9.1.20.tgz", + "integrity": "sha512-HHgk50YQhML7mT01Mzf9N7lNMFHWN4HwwRP90kPT9Ct+Jhx7h3LBDbdmWjI96HwujcpY7eoYdTfpB1Sw8Z7nBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "unplugin": "^1.3.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^9.1.20" + } + }, + "node_modules/@storybook/vue3-vite/node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@storybook/vue3/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@storybook/web-components": { "version": "9.1.16", "dev": true, @@ -5510,6 +5646,7 @@ }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -7125,6 +7262,13 @@ "dev": true, "license": "MIT" }, + "node_modules/assert-never": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.4.0.tgz", + "integrity": "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==", + "dev": true, + "license": "MIT" + }, "node_modules/assertion-error": { "version": "2.0.1", "dev": true, @@ -7340,6 +7484,19 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/babel-walk": { + "version": "3.0.0-canary-5", + "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", + "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.9.6" + }, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "license": "MIT" @@ -7934,6 +8091,16 @@ "node": ">=10" } }, + "node_modules/character-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", + "integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-regex": "^1.0.3" + } + }, "node_modules/chardet": { "version": "2.1.1", "dev": true, @@ -8537,6 +8704,17 @@ "node": ">= 0.10.0" } }, + "node_modules/constantinople": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", + "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.6.0", + "@babel/types": "^7.6.1" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "license": "MIT", @@ -9615,6 +9793,7 @@ }, "node_modules/detect-libc": { "version": "2.1.2", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -9685,6 +9864,13 @@ "node": ">=6.0.0" } }, + "node_modules/doctypes": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", + "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==", + "dev": true, + "license": "MIT" + }, "node_modules/dom-accessibility-api": { "version": "0.5.16", "dev": true, @@ -10085,7 +10271,7 @@ "version": "0.27.7", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", - "devOptional": true, + "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -10506,6 +10692,13 @@ "node": ">= 8" } }, + "node_modules/esm-resolve": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/esm-resolve/-/esm-resolve-1.0.11.tgz", + "integrity": "sha512-LxF0wfUQm3ldUDHkkV2MIbvvY0TgzIpJ420jHSV1Dm+IlplBEWiJTKWM61GtxUfvjV6iD4OtTYFGAGM2uuIUWg==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/espree": { "version": "9.6.1", "license": "BSD-2-Clause", @@ -11002,6 +11195,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-package-json": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/find-package-json/-/find-package-json-1.2.0.tgz", + "integrity": "sha512-+SOGcLGYDJHtyqHd87ysBhmaeQ95oWspDKnMXBrnQ9Eq4OkLNqejgoaD8xVWu6GPa0B6roa6KinCMEMcVeqONw==", + "dev": true, + "license": "MIT" + }, "node_modules/find-replace": { "version": "3.0.0", "dev": true, @@ -12723,6 +12923,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-expression": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz", + "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^7.1.1", + "object-assign": "^4.1.1" + } + }, + "node_modules/is-expression/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "license": "MIT", @@ -12901,6 +13125,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "dev": true, @@ -13314,7 +13545,7 @@ }, "node_modules/jiti": { "version": "2.6.1", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -13343,6 +13574,13 @@ "jquery": ">=1.7" } }, + "node_modules/js-stringify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", + "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==", + "dev": true, + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "license": "MIT" @@ -13425,6 +13663,17 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jstransformer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", + "integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-promise": "^2.0.0", + "promise": "^7.0.1" + } + }, "node_modules/junk": { "version": "1.0.3", "dev": true, @@ -13537,6 +13786,7 @@ }, "node_modules/lightningcss": { "version": "1.32.0", + "dev": true, "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -13569,6 +13819,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -13587,6 +13838,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -13607,6 +13859,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -13627,6 +13880,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -13647,6 +13901,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -13667,6 +13922,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -13687,6 +13943,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -13707,6 +13964,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -13727,6 +13985,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -13747,6 +14006,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -13767,6 +14027,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -16791,6 +17052,142 @@ "version": "1.0.2", "license": "ISC" }, + "node_modules/pug": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.4.tgz", + "integrity": "sha512-kFfq5mMzrS7+wrl5pLJzZEzemx34OQ0w4SARfhy/3yxTlhbstsudDwJzhf1hP02yHzbjoVMSXUj/Sz6RNfMyXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pug-code-gen": "^3.0.4", + "pug-filters": "^4.0.0", + "pug-lexer": "^5.0.1", + "pug-linker": "^4.0.0", + "pug-load": "^3.0.0", + "pug-parser": "^6.0.0", + "pug-runtime": "^3.0.1", + "pug-strip-comments": "^2.0.0" + } + }, + "node_modules/pug-attrs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz", + "integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "constantinople": "^4.0.1", + "js-stringify": "^1.0.2", + "pug-runtime": "^3.0.0" + } + }, + "node_modules/pug-code-gen": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.4.tgz", + "integrity": "sha512-6okWYIKdasTyXICyEtvobmTZAVX57JkzgzIi4iRJlin8kmhG+Xry2dsus+Mun/nGCn6F2U49haHI5mkELXB14g==", + "dev": true, + "license": "MIT", + "dependencies": { + "constantinople": "^4.0.1", + "doctypes": "^1.1.0", + "js-stringify": "^1.0.2", + "pug-attrs": "^3.0.0", + "pug-error": "^2.1.0", + "pug-runtime": "^3.0.1", + "void-elements": "^3.1.0", + "with": "^7.0.0" + } + }, + "node_modules/pug-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.1.0.tgz", + "integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==", + "dev": true, + "license": "MIT" + }, + "node_modules/pug-filters": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-4.0.0.tgz", + "integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "constantinople": "^4.0.1", + "jstransformer": "1.0.0", + "pug-error": "^2.0.0", + "pug-walk": "^2.0.0", + "resolve": "^1.15.1" + } + }, + "node_modules/pug-lexer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.1.tgz", + "integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-parser": "^2.2.0", + "is-expression": "^4.0.0", + "pug-error": "^2.0.0" + } + }, + "node_modules/pug-linker": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz", + "integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pug-error": "^2.0.0", + "pug-walk": "^2.0.0" + } + }, + "node_modules/pug-load": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz", + "integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.1", + "pug-walk": "^2.0.0" + } + }, + "node_modules/pug-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz", + "integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pug-error": "^2.0.0", + "token-stream": "1.0.0" + } + }, + "node_modules/pug-runtime": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz", + "integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==", + "dev": true, + "license": "MIT" + }, + "node_modules/pug-strip-comments": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz", + "integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pug-error": "^2.0.0" + } + }, + "node_modules/pug-walk": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz", + "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==", + "dev": true, + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "license": "MIT", @@ -20222,6 +20619,13 @@ "node": ">=0.6" } }, + "node_modules/token-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", + "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==", + "dev": true, + "license": "MIT" + }, "node_modules/totalist": { "version": "3.0.1", "dev": true, @@ -20344,6 +20748,13 @@ "node": ">=8" } }, + "node_modules/ts-map": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-map/-/ts-map-1.0.3.tgz", + "integrity": "sha512-vDWbsl26LIcPGmDpoVzjEP6+hvHZkBkLW7JpvwbCv/5IYPJlsbzCVXY3wsCeAxAUeTclNOUZxnLdGh3VBD/J6w==", + "dev": true, + "license": "MIT" + }, "node_modules/tsconfck": { "version": "3.1.6", "dev": true, @@ -20634,6 +21045,7 @@ }, "node_modules/typescript": { "version": "6.0.2", + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -21051,6 +21463,7 @@ }, "node_modules/vite": { "version": "8.0.3", + "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", @@ -21277,6 +21690,7 @@ }, "node_modules/vite/node_modules/@oxc-project/types": { "version": "0.122.0", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" @@ -21287,6 +21701,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -21298,10 +21713,12 @@ }, "node_modules/vite/node_modules/@rolldown/pluginutils": { "version": "1.0.0-rc.12", + "dev": true, "license": "MIT" }, "node_modules/vite/node_modules/fsevents": { "version": "2.3.3", + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -21313,6 +21730,7 @@ }, "node_modules/vite/node_modules/rolldown": { "version": "1.0.0-rc.12", + "dev": true, "license": "MIT", "dependencies": { "@oxc-project/types": "=0.122.0", @@ -21498,6 +21916,16 @@ } } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/vscode-languageserver-types": { "version": "3.17.5", "license": "MIT" @@ -21567,6 +21995,169 @@ "vue": ">=2" } }, + "node_modules/vue-component-meta": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/vue-component-meta/-/vue-component-meta-2.2.12.tgz", + "integrity": "sha512-dQU6/obNSNbennJ1xd+rhDid4g3vQro+9qUBBIg8HMZH2Zs1jTpkFNxuQ3z77bOlU+ew08Qck9sbYkdSePr0Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.15", + "@vue/language-core": "2.2.12", + "path-browserify": "^1.0.1", + "vue-component-type-helpers": "2.2.12" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-meta/node_modules/@volar/language-core": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz", + "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.15" + } + }, + "node_modules/vue-component-meta/node_modules/@volar/source-map": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz", + "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue-component-meta/node_modules/@volar/typescript": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz", + "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/vue-component-meta/node_modules/@vue/language-core": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz", + "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^1.0.3", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-meta/node_modules/alien-signals": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz", + "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue-component-meta/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/vue-component-meta/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/vue-component-meta/node_modules/vue-component-type-helpers": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.2.12.tgz", + "integrity": "sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue-component-type-helpers": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.2.7.tgz", + "integrity": "sha512-+gPp5YGmhfsj1IN+xUo7y0fb4clfnOiiUA39y07yW1VzCRjzVgwLbtmdWlghh7mXrPsEaYc7rrIir/HT6C8vYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue-docgen-api": { + "version": "4.79.2", + "resolved": "https://registry.npmjs.org/vue-docgen-api/-/vue-docgen-api-4.79.2.tgz", + "integrity": "sha512-n9ENAcs+40awPZMsas7STqjkZiVlIjxIKgiJr5rSohDP0/JCrD9VtlzNojafsA1MChm/hz2h3PDtUedx3lbgfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", + "@vue/compiler-dom": "^3.2.0", + "@vue/compiler-sfc": "^3.2.0", + "ast-types": "^0.16.1", + "esm-resolve": "^1.0.8", + "hash-sum": "^2.0.0", + "lru-cache": "^8.0.3", + "pug": "^3.0.2", + "recast": "^0.23.1", + "ts-map": "^1.0.3", + "vue-inbrowser-compiler-independent-utils": "^4.69.0" + }, + "peerDependencies": { + "vue": ">=2" + } + }, + "node_modules/vue-docgen-api/node_modules/hash-sum": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-2.0.0.tgz", + "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue-docgen-api/node_modules/lru-cache": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", + "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16.14" + } + }, "node_modules/vue-eslint-parser": { "version": "9.4.3", "license": "MIT", @@ -21634,6 +22225,16 @@ "version": "2.3.4", "license": "MIT" }, + "node_modules/vue-inbrowser-compiler-independent-utils": { + "version": "4.71.1", + "resolved": "https://registry.npmjs.org/vue-inbrowser-compiler-independent-utils/-/vue-inbrowser-compiler-independent-utils-4.71.1.tgz", + "integrity": "sha512-K3wt3iVmNGaFEOUR4JIThQRWfqokxLfnPslD41FDZB2ajXp789+wCqJyGYlIFsvEQ2P61PInw6/ph5iiqg51gg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "vue": ">=2" + } + }, "node_modules/vue-loader": { "version": "15.11.1", "license": "MIT", @@ -22392,6 +22993,22 @@ "version": "2.0.1", "license": "MIT" }, + "node_modules/with": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz", + "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.9.6", + "@babel/types": "^7.9.6", + "assert-never": "^1.2.1", + "babel-walk": "3.0.0-canary-5" + }, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "license": "MIT", @@ -22600,7 +23217,7 @@ }, "node_modules/yaml": { "version": "2.8.3", - "devOptional": true, + "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/package.json b/package.json index 7bd1a3ce207..59142d3d4aa 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "dev:cp": "cd ./packages/craftcms-cp && npm run dev", "build:cp": "cd ./packages/craftcms-cp && npm run build", "test:cp": "cd ./packages/craftcms-cp && npm run test", + "storybook": "storybook dev -p 6007", + "build:storybook": "storybook build", "storybook:cp": "cd ./packages/craftcms-cp && npm run storybook", "build:all": "npm run build:bundles && npm run build:cp && npm run build" }, @@ -28,6 +30,11 @@ "devDependencies": { "@craftcms/playwright": "file:packages/craftcms-playwright", "@craftcms/webpack": "file:packages/craftcms-webpack", + "@storybook/addon-a11y": "^9.1.5", + "@storybook/addon-docs": "^9.1.5", + "@storybook/addon-themes": "^9.1.5", + "@storybook/vue3-vite": "^9.1.5", + "storybook": "^9.1.5", "@laravel/vite-plugin-wayfinder": "^0.1.7", "@playwright/test": "^1.58.2", "@tailwindcss/vite": "^4.2.2", diff --git a/packages/craftcms-cp/scripts/generate-colors.js b/packages/craftcms-cp/scripts/generate-colors.js index 5533c2423f7..d8e1cab76af 100644 --- a/packages/craftcms-cp/scripts/generate-colors.js +++ b/packages/craftcms-cp/scripts/generate-colors.js @@ -33,6 +33,17 @@ const availableColors = [ 'black', ]; +const semanticColors = [ + // Semantic colors + 'neutral', + 'brand', + 'accent', + 'info', + 'success', + 'warning', + 'danger', +]; + function lightScale(color) { switch (color) { case 'white': @@ -136,7 +147,7 @@ function buildTokens(colors, scaleFn) { } function buildStyleBlock(color) { - return `.c-colorable--${color}, + return `.cp-color-${color}, [data-color='${color}'] { --c-color-fill-quiet: var(--c-color-${color}-fill-quiet); --c-color-border-quiet: var(--c-color-${color}-border-quiet); @@ -161,24 +172,7 @@ ${buildTokens(colors, lightScale)} ${buildTokens(colors, darkScale)} } -.c-colorable, -[data-color] { - --c-color-fill-quiet: var(--c-color-neutral-fill-quiet); - --c-color-fill-normal: var(--c-color-neutral-fill-normal); - --c-color-fill-loud: var(--c-color-neutral-fill-loud); - --c-color-border-quiet: var(--c-color-neutral-border-quiet); - --c-color-border-normal: var(--c-color-neutral-border-normal); - --c-color-border-loud: var(--c-color-neutral-border-loud); - --c-color-on-quiet: var(--c-color-neutral-on-quiet); - --c-color-on-normal: var(--c-color-neutral-on-normal); - --c-color-on-loud: var(--c-color-neutral-on-loud); - - background-color: var(--c-color-fill-quiet); - border-color: var(--c-color-border-quiet); - color: var(--c-color-on-quiet); -} - -${colors.map((c) => buildStyleBlock(c)).join('\n')} +${[...availableColors, ...semanticColors].map((c) => buildStyleBlock(c)).join('\n')} `; } diff --git a/packages/craftcms-cp/scripts/generate-vue-wrappers.js b/packages/craftcms-cp/scripts/generate-vue-wrappers.js index f494c43792e..7e190849fc7 100644 --- a/packages/craftcms-cp/scripts/generate-vue-wrappers.js +++ b/packages/craftcms-cp/scripts/generate-vue-wrappers.js @@ -230,6 +230,10 @@ function generateValueWrapper(component) { }); const model = defineModel<${component.modelType}>(); + + defineProps<{ + error?: null | string + }>() `; diff --git a/packages/craftcms-cp/src/components/action-item/action-item.stories.ts b/packages/craftcms-cp/src/components/action-item/action-item.stories.ts index ee95959c0d9..96bf51aab24 100644 --- a/packages/craftcms-cp/src/components/action-item/action-item.stories.ts +++ b/packages/craftcms-cp/src/components/action-item/action-item.stories.ts @@ -82,6 +82,26 @@ export const Link: Story = { }, }; +export const WithShortcut: Story = { + args: {}, + render({icon, active, href, checked}) { + return html` + + Save + + `; + }, +}; + +export const WithComplexShortcut: Story = { + args: {}, + render() { + return html` + Save + `; + }, +}; + export const CheckableWithIcon: Story = { args: { checked: true, diff --git a/packages/craftcms-cp/src/components/action-item/action-item.styles.ts b/packages/craftcms-cp/src/components/action-item/action-item.styles.ts index 31b1cbaa20e..70b095a1076 100644 --- a/packages/craftcms-cp/src/components/action-item/action-item.styles.ts +++ b/packages/craftcms-cp/src/components/action-item/action-item.styles.ts @@ -68,6 +68,10 @@ export default css` .action-item__suffix { align-self: center; } + + craft-shortcut { + margin-inline-start: var(--c-spacing-sm); + } .action-item__label { flex: 1 1 auto; diff --git a/packages/craftcms-cp/src/components/action-item/action-item.ts b/packages/craftcms-cp/src/components/action-item/action-item.ts index 5c04af54cc4..91d6145c98e 100644 --- a/packages/craftcms-cp/src/components/action-item/action-item.ts +++ b/packages/craftcms-cp/src/components/action-item/action-item.ts @@ -5,6 +5,8 @@ import {Variant, type VariantKey} from '@src/types'; import variantsStyles from '@src/styles/variants.styles'; import {classMap} from 'lit/directives/class-map.js'; +import '../shortcut/shortcut.js'; + /** * @summary Either a link or button typically used in a menu. */ @@ -18,6 +20,49 @@ export default class CraftActionItem extends LitElement { @property({type: Boolean}) active: boolean = false; @property() type: 'normal' | 'checkbox' = 'normal'; + @property({ + converter: { + fromAttribute(value: string | null) { + if (value === null) return null; + + // Try to parse as JSON object first + try { + const parsed = JSON.parse(value); + if (typeof parsed === 'object' && parsed !== null) { + return parsed; + } + } catch { + // Not JSON — treat as plain string shortcut + } + + return value; // plain string like "k" or "ctrl+k" + }, + toAttribute(value) { + if (value === null) return null; + if (typeof value === 'string') return value; + return JSON.stringify(value); + }, + }, + }) + shortcut: string | {alt?: boolean; shift?: boolean; key: string} | null = + null; + + renderShortcut() { + if (typeof this.shortcut === 'string') { + return html`${this.shortcut}`; + } + + if (this.shortcut !== null) { + return html`${this.shortcut.key}`; + } + + return nothing; + } + renderBody() { const hasIcon = !!this.querySelector('[slot="icon"]') || !!this.icon; @@ -47,6 +92,7 @@ export default class CraftActionItem extends LitElement { + ${this.shortcut ? this.renderShortcut() : nothing} `; } diff --git a/packages/craftcms-cp/src/components/button/button.ts b/packages/craftcms-cp/src/components/button/button.ts index c988d3013b0..aef386b6dc8 100644 --- a/packages/craftcms-cp/src/components/button/button.ts +++ b/packages/craftcms-cp/src/components/button/button.ts @@ -80,11 +80,15 @@ export default class CraftButton extends LionButtonSubmit { /** Set align-items for the content */ @property() align: 'start' | 'end' | 'center' = 'center'; + @property() icon: string | null = null; + @state() private _hasAccessibilityError: boolean = false; override render() { return html` + +
- + + ${this.icon + ? html`` + : nothing} +
diff --git a/packages/craftcms-cp/src/components/checkbox/checkbox.ts b/packages/craftcms-cp/src/components/checkbox/checkbox.ts index 191568c8b4c..7f3ebca79b5 100644 --- a/packages/craftcms-cp/src/components/checkbox/checkbox.ts +++ b/packages/craftcms-cp/src/components/checkbox/checkbox.ts @@ -8,9 +8,10 @@ export default class CraftCheckbox extends LionCheckbox { css` /* same as radio, potentially consolidate */ :host { + --_gap-x: var(--gap-x, --c-spacing-md); display: grid; align-items: center; - gap: 0 var(--c-spacing-md); + gap: 0 var(--_gap-x); grid-template-areas: 'input label' '. help-text'; grid-template-columns: auto 1fr; grid-template-rows: repeat(2, auto); @@ -36,6 +37,8 @@ export default class CraftCheckbox extends LionCheckbox { var(--c-form-control-border-color) ); border-radius: var(--c-input-radius, var(--c-radius-sm)); + width: var(--c-size-control-2xs); + height: var(--c-size-control-2xs); } .choice-field__help-text { diff --git a/packages/craftcms-cp/src/components/chip/chip.styles.ts b/packages/craftcms-cp/src/components/chip/chip.styles.ts index 9f9d7888532..5889249eec8 100644 --- a/packages/craftcms-cp/src/components/chip/chip.styles.ts +++ b/packages/craftcms-cp/src/components/chip/chip.styles.ts @@ -5,12 +5,12 @@ export default css` display: contents; } - .chip { + .cp-chip { + --_min-height: var(--c-chip-height, var(--c-size-control-sm)); display: inline-flex; - min-height: var(--c-chip-height, var(--c-size-control-sm)); min-width: auto; border-radius: var(--c-chip-radius, var(--c-radius-md)); - padding-inline: var(--c-chip-spacing-inline, var(--c-spacing-md)); + padding-inline: var(--c-chip-spacing-inline, 0); padding-block: var(--c-chip-spacing-block, var(--c-spacing-sm)); align-items: start; box-shadow: var(--c-chip-shadow, var(--c-shadow-sm)); @@ -26,8 +26,14 @@ export default css` background-color: var(--c-color-fill-quiet, var(--c-surface-raised)); } - .chip[appearance='plain'], - .chip--plain { + .cp-chip__body ::slotted(a) { + text-decoration: none; + font-weight: bold; + display: flex; + } + + .cp-chip[appearance='plain'], + .cp-chip--plain { padding-block: 0; padding-inline: 0; border-color: transparent; @@ -35,42 +41,38 @@ export default css` box-shadow: none; } - .chip[size='small'], - .chip--small { - padding-block: 0; - min-height: var(--c-size-control-sm); + .cp-chip[size='small'], + .cp-chip--small { + --_min-height: var(--c-size-control-sm); + padding-block: calc(var(--c-spacing-xs) / 2); } - chip[size='medium'], - .chip--medium { + .cp-chip[size='medium'], + .cp-chip--medium { padding-block: 0; min-height: var(--c-size-control-md); } - .chip__prefix, - .chip__body, - .chip__suffix { + .cp-chip__prefix, + .cp-chip__body, + .cp-chip__suffix { display: inline-flex; flex-direction: column; - align-self: center; + min-height: var(--_min-height); } - .chip__body { + .cp-chip__body { flex: 1 1 auto; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } - .chip__prefix { - padding-inline-end: var(--c-spacing-md); + .cp-chip__prefix { + padding-inline: calc(var(--c-spacing-md) / 2); } - .chip__suffix { + .cp-chip__suffix { padding-inline-start: var(--c-spacing-md); } - - :host(:not([variant='plain'])) .chip__suffix { - margin-inline-end: calc(var(--c-spacing-sm) * -1); - } `; diff --git a/packages/craftcms-cp/src/components/chip/chip.ts b/packages/craftcms-cp/src/components/chip/chip.ts index f8611c990f5..cdf57e9ebf7 100644 --- a/packages/craftcms-cp/src/components/chip/chip.ts +++ b/packages/craftcms-cp/src/components/chip/chip.ts @@ -29,37 +29,42 @@ export default class CraftChip extends LitElement { @property() icon: string | null = null; renderPrefix() { - return html`
+ return html`
- ${this.icon - ? html`` - : nothing} + + ${this.icon + ? html`` + : nothing} +
`; } override render() { // query the element Light DOM children for slotted elements - const renderPrefix = !!this.querySelector('[slot="prefix"]') || this.icon; + const renderPrefix = + !!this.querySelector('[slot="prefix"]') || + !!this.querySelector('[slot="icon"]') || + this.icon; const renderSuffix = !!this.querySelector('[slot="suffix"]'); return html`
${renderPrefix ? this.renderPrefix() : nothing} -
+
${renderSuffix - ? html`
+ ? html`
` : nothing} diff --git a/packages/craftcms-cp/src/components/copy-attribute/copy-attribute.styles.ts b/packages/craftcms-cp/src/components/copy-attribute/copy-attribute.styles.ts index 73688740c3b..c638dfe8e2f 100644 --- a/packages/craftcms-cp/src/components/copy-attribute/copy-attribute.styles.ts +++ b/packages/craftcms-cp/src/components/copy-attribute/copy-attribute.styles.ts @@ -60,16 +60,4 @@ export default css` color: var(--c-copy-attribute-error-text, var(--c-copy-attribute-text)); border: var(--c-copy-attribute-error-border, var(--_border)); } - - .icon { - display: inline-block; - width: 0.9em; - height: 0.9em; - } - - svg { - fill: currentColor; - width: 100%; - height: 100%; - } `; diff --git a/packages/craftcms-cp/src/components/copy-attribute/copy-attribute.ts b/packages/craftcms-cp/src/components/copy-attribute/copy-attribute.ts index 2fe6abea3a5..229bbe6f008 100644 --- a/packages/craftcms-cp/src/components/copy-attribute/copy-attribute.ts +++ b/packages/craftcms-cp/src/components/copy-attribute/copy-attribute.ts @@ -9,23 +9,6 @@ import styles from './copy-attribute.styles.js'; import type {CSSResultGroup} from 'lit'; import CraftCopyButton from '../copy-button/copy-button.js'; -const animations = { - 'icon.in': { - keyframes: [ - {scale: 0.25, opacity: 0.25}, - {scale: 1, opacity: 1}, - ], - options: {duration: 100}, - }, - 'icon.out': { - keyframes: [ - {scale: 1, opacity: 1}, - {scale: 0.25, opacity: 0.25}, - ], - options: {duration: 100}, - }, -}; - /** * @summary Displays a field handle and allows quick copying * @@ -37,11 +20,6 @@ const animations = { export default class CraftCopyAttribute extends LitElement { static override styles: CSSResultGroup = [hostStyles, styles]; - @state() status: 'rest' | 'success' | 'error' = 'rest'; - - @query('slot[name="copy-icon"]') copyIconEl!: HTMLSlotElement; - @query('slot[name="success-icon"]') successIconEl!: HTMLSlotElement; - @query('slot[name="error-icon"]') errorIconEl!: HTMLSlotElement; @query('craft-copy-button') copyButtonEl!: CraftCopyButton; /** The text value to copy */ @@ -52,23 +30,6 @@ export default class CraftCopyAttribute extends LitElement { @property({type: Boolean, reflect: true}) disabled: boolean = false; - /** The length of time to show feedback before restoring the default trigger. */ - @property({attribute: 'feedback-duration', type: Number}) feedbackDuration = - 1000; - - @property({reflect: false}) - tooltipLabel: string = 'Copy'; - - constructor() { - super(); - this.addEventListener('craft-copy', () => { - this.showStatus('success'); - }); - this.addEventListener('craft-error', () => { - this.showStatus('error'); - }); - } - getId(): string { return `attribute-${this.value .replace(/([a-z])([A-Z])/g, '$1-$2') @@ -76,79 +37,16 @@ export default class CraftCopyAttribute extends LitElement { .toLowerCase()}`; } - async showStatus(status: 'success' | 'error') { - const statusIcon = - status === 'success' ? this.successIconEl : this.errorIconEl; - this.tooltipLabel = status === 'success' ? 'Copied' : 'Copy failed'; - - // Animate the copy icon out - await statusIcon.animate( - animations['icon.out'].keyframes, - animations['icon.out'].options - ); - this.copyIconEl.hidden = true; - - // Animate the status icon in - statusIcon.hidden = false; - await statusIcon.animate( - animations['icon.in'].keyframes, - animations['icon.in'].options - ); - - this.status = status; - - // Put everything back - setTimeout(async () => { - // Animate the status icon out - await statusIcon.animate( - animations['icon.out'].keyframes, - animations['icon.out'].options - ); - statusIcon.hidden = true; - - // Animate the copy icon in - this.copyIconEl.hidden = false; - await this.copyIconEl.animate( - animations['icon.in'].keyframes, - animations['icon.in'].options - ); - - this.status = 'rest'; - this.tooltipLabel = 'Copy'; - }, this.feedbackDuration); - } - override render() { return html` - ${this.tooltipLabel} ${this.value} - - - - - - - - - - - - `; } diff --git a/packages/craftcms-cp/src/components/copy-button/copy-button.styles.ts b/packages/craftcms-cp/src/components/copy-button/copy-button.styles.ts index b02da8c7075..4bc7a3d5309 100644 --- a/packages/craftcms-cp/src/components/copy-button/copy-button.styles.ts +++ b/packages/craftcms-cp/src/components/copy-button/copy-button.styles.ts @@ -18,4 +18,16 @@ export default css` border: none; cursor: pointer; } + + .icon { + display: inline-block; + width: 0.9em; + height: 0.9em; + } + + svg { + fill: currentColor; + width: 100%; + height: 100%; + } `; diff --git a/packages/craftcms-cp/src/components/copy-button/copy-button.ts b/packages/craftcms-cp/src/components/copy-button/copy-button.ts index a445f8dfe03..29438d147a2 100644 --- a/packages/craftcms-cp/src/components/copy-button/copy-button.ts +++ b/packages/craftcms-cp/src/components/copy-button/copy-button.ts @@ -1,8 +1,27 @@ -import {property, state} from 'lit/decorators.js'; +import {t} from '@src/utilities/translate'; +import {property, query, state} from 'lit/decorators.js'; import {html, LitElement} from 'lit'; -import styles from './copy-button.styles.js'; -import '@shoelace-style/shoelace/dist/components/visually-hidden/visually-hidden.js'; import type {CSSResultGroup} from 'lit'; +import styles from './copy-button.styles.js'; +import '../tooltip/tooltip.js'; +import '../visually-hidden/visually-hidden.js'; + +const animations = { + 'icon.in': { + keyframes: [ + {scale: 0.25, opacity: 0.25}, + {scale: 1, opacity: 1}, + ], + options: {duration: 100}, + }, + 'icon.out': { + keyframes: [ + {scale: 1, opacity: 1}, + {scale: 0.25, opacity: 0.25}, + ], + options: {duration: 100}, + }, +}; /** * @summary Copy values to the clipboard on click. @@ -19,17 +38,30 @@ export default class CraftCopyButton extends LitElement { @state() isCopying = false; + @state() status: 'rest' | 'copying' | 'success' | 'error' = 'rest'; + + @query('slot[name="copy-icon"]') copyIconEl!: HTMLSlotElement; + @query('slot[name="success-icon"]') successIconEl!: HTMLSlotElement; + @query('slot[name="error-icon"]') errorIconEl!: HTMLSlotElement; + /** Value to copy on click */ @property({type: String}) value = ''; @property({type: Boolean}) disabled = false; + /** The length of time to show feedback before restoring the default trigger. */ + @property({attribute: 'feedback-duration', type: Number}) feedbackDuration = + 1000; + + @property() + tooltipLabel: string | null = null; + async copyValue() { - if (this.isCopying || this.disabled) { + if (this.status === 'copying' || this.disabled) { return; } - this.isCopying = true; + this.status = 'copying'; try { await navigator.clipboard.writeText(this.value); @@ -56,17 +88,96 @@ export default class CraftCopyButton extends LitElement { } } + async showStatus(status: 'success' | 'error') { + const statusIcon = + status === 'success' ? this.successIconEl : this.errorIconEl; + this.tooltipLabel = status === 'success' ? 'Copied' : 'Copy failed'; + + // Animate the copy icon out + await statusIcon.animate( + animations['icon.out'].keyframes, + animations['icon.out'].options + ); + this.copyIconEl.hidden = true; + + // Animate the status icon in + statusIcon.hidden = false; + await statusIcon.animate( + animations['icon.in'].keyframes, + animations['icon.in'].options + ); + + this.status = status; + + // Put everything back + setTimeout(async () => { + // Animate the status icon out + await statusIcon.animate( + animations['icon.out'].keyframes, + animations['icon.out'].options + ); + statusIcon.hidden = true; + + // Animate the copy icon in + this.copyIconEl.hidden = false; + await this.copyIconEl.animate( + animations['icon.in'].keyframes, + animations['icon.in'].options + ); + + this.status = 'rest'; + this.tooltipLabel = 'Copy'; + }, this.feedbackDuration); + } + override connectedCallback() { + super.connectedCallback(); + + this.tooltipLabel = this.getAttribute('tooltip-label') || t('Copy'); + + if (!this.id) { + this.id = `copy-${Math.floor(Math.random() * 100000000)}`; + } + + this.addEventListener('craft-copy', () => { + this.showStatus('success'); + }); + + this.addEventListener('craft-error', () => { + this.showStatus('error'); + }); + } + override render() { return html` + ${this.tooltipLabel} `; } diff --git a/packages/craftcms-cp/src/components/icon/icon.ts b/packages/craftcms-cp/src/components/icon/icon.ts index 85988229844..99ec23366fe 100644 --- a/packages/craftcms-cp/src/components/icon/icon.ts +++ b/packages/craftcms-cp/src/components/icon/icon.ts @@ -1,5 +1,6 @@ import WaIcon from '@awesome.me/webawesome/dist/components/icon/icon.js'; import {css} from 'lit'; +import {property} from 'lit/decorators.js'; /** * craft-icon is just an alias to wa-icon from web awesome. @@ -7,6 +8,16 @@ import {css} from 'lit'; * Anything you can do over there you can do here. */ export default class CraftIcon extends WaIcon { + @property({reflect: true}) appearance?: 'plain' | 'badge' = 'plain'; + + override connectedCallback() { + super.connectedCallback(); + + if (this.appearance === 'badge' && !this.getAttribute('data-color')) { + this.setAttribute('data-color', 'warning'); + } + } + static override get styles() { return [ WaIcon.styles, @@ -14,6 +25,19 @@ export default class CraftIcon extends WaIcon { :host { font-size: 0.8em; } + + :host([appearance~='badge']) { + border: 1px solid var(--c-color-border-quiet); + color: var(--c-color-on-quiet); + background-color: var(--c-color-fill-quiet); + border-radius: var(--c-radius-sm); + width: 1.6em; + height: 1.6em; + + svg { + width: 0.9em; + } + } `, ]; } diff --git a/packages/craftcms-cp/src/components/info-icon/info-icon.stories.ts b/packages/craftcms-cp/src/components/info-icon/info-icon.stories.ts new file mode 100644 index 00000000000..f17461b287b --- /dev/null +++ b/packages/craftcms-cp/src/components/info-icon/info-icon.stories.ts @@ -0,0 +1,36 @@ +import type {Meta, StoryObj} from '@storybook/web-components-vite'; +import './info-icon'; + +const meta: Meta = { + title: 'Components/Info Icon', + tags: ['autodocs'], + args: {}, + render: (args) => { + return ` + + This is the content for the tooltip + `; + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + label: 'More Info', + icon: 'circle-info', + }, +}; + +export const Multiple: Story = { + render: () => { + return ` +
+ Tooltip content for icon 1 + Tooltip content for icon 2 + Tooltip content for icon 3 +
`; + }, +}; diff --git a/packages/craftcms-cp/src/components/info-icon/info-icon.ts b/packages/craftcms-cp/src/components/info-icon/info-icon.ts new file mode 100644 index 00000000000..cb2364b98dd --- /dev/null +++ b/packages/craftcms-cp/src/components/info-icon/info-icon.ts @@ -0,0 +1,128 @@ +import {t} from '@src/utilities/translate'; +import {css, html, LitElement} from 'lit'; +import {property, query, queryAssignedElements, state} from 'lit/decorators.js'; + +import '../button/button'; +import '../icon/icon'; +import '../tooltip/tooltip'; +import type CraftTooltip from '../tooltip/tooltip'; +import '../visually-hidden/visually-hidden'; + +export default class CraftInfoIcon extends LitElement { + static override styles = css` + :host { + display: inline-flex; + } + `; + + static #openInstance: CraftInfoIcon | null = null; + + @property() label = t('More Info'); + + @property() icon = 'circle-info'; + + @property({type: Boolean, reflect: true}) disabled = false; + + @property() override id: string; + + @state() status = ''; + + @query('c-tooltip') tooltip!: HTMLElement; + + #eventController = new AbortController(); + + override connectedCallback() { + super.connectedCallback(); + + // Recreate event controller if it was aborted + if (this.#eventController.signal.aborted) { + this.#eventController = new AbortController(); + } + + if (!this.id) { + this.id = `info-icon-${Math.random().toString(36).slice(2, 8)}`; + } + + const {signal} = this.#eventController; + + this.addEventListener( + 'wa-show', + () => { + if ( + CraftInfoIcon.#openInstance && + CraftInfoIcon.#openInstance !== this + ) { + const otherTooltip = + CraftInfoIcon.#openInstance.renderRoot.querySelector( + 'c-tooltip' + ); + otherTooltip?.hide(); + } + CraftInfoIcon.#openInstance = this; + }, + {signal} + ); + + this.addEventListener( + 'wa-after-show', + () => { + this.status = ''; + setTimeout(() => { + this.status = 'Some new status'; + }, 200); + }, + {signal} + ); + + this.addEventListener( + 'wa-after-hide', + () => { + if (CraftInfoIcon.#openInstance === this) { + CraftInfoIcon.#openInstance = null; + } + this.status = ''; + }, + {signal} + ); + } + + override disconnectedCallback() { + if (CraftInfoIcon.#openInstance === this) { + CraftInfoIcon.#openInstance = null; + } + this.#eventController.abort(); + super.disconnectedCallback(); + } + + override render() { + return html` +
+ + ${this.status} + + + + + + + +
+ `; + } +} + +if (!customElements.get('craft-info-icon')) { + customElements.define('craft-info-icon', CraftInfoIcon); +} + +declare global { + interface HTMLElementTagNameMap { + 'craft-info-icon': CraftInfoIcon; + } +} diff --git a/packages/craftcms-cp/src/components/spinner/spinner.ts b/packages/craftcms-cp/src/components/spinner/spinner.ts index c79fea95a80..caaa291af3a 100644 --- a/packages/craftcms-cp/src/components/spinner/spinner.ts +++ b/packages/craftcms-cp/src/components/spinner/spinner.ts @@ -3,6 +3,8 @@ import {property, query} from 'lit/decorators.js'; import componentStyles from './spinner.styles.js'; import {classMap} from 'lit/directives/class-map.js'; +import '../visually-hidden/visually-hidden'; + export default class CraftSpinner extends LitElement { static override styles = [componentStyles]; @@ -36,7 +38,7 @@ export default class CraftSpinner extends LitElement { })}" >
- +
`; } diff --git a/packages/craftcms-cp/src/components/tooltip/tooltip.stories.ts b/packages/craftcms-cp/src/components/tooltip/tooltip.stories.ts index 600bfe471f1..556f01f38a8 100644 --- a/packages/craftcms-cp/src/components/tooltip/tooltip.stories.ts +++ b/packages/craftcms-cp/src/components/tooltip/tooltip.stories.ts @@ -24,11 +24,11 @@ const meta = { } return html` - ${args.content}${args.content} Hover me `; diff --git a/packages/craftcms-cp/src/components/tooltip/tooltip.ts b/packages/craftcms-cp/src/components/tooltip/tooltip.ts index c25fe343fd0..d4f7d2a836e 100644 --- a/packages/craftcms-cp/src/components/tooltip/tooltip.ts +++ b/packages/craftcms-cp/src/components/tooltip/tooltip.ts @@ -12,37 +12,31 @@ export default class CraftTooltip extends WaTooltip { return [ WaTooltip.styles, css` - wa-popup { - --wa-z-index-tooltip: var(--c-tooltip-z-index, 1000); - --wa-tooltip-background-color: var( - --c-tooltip-fill, - var(--c-surface-overlay) - ); - --wa-tooltip-border-color: var( - --c-tooltip-border, - var(--c-color-neutral-border-quiet) - ); - --wa-tooltip-content-color: var(--c-tooltip-text, currentColor); + :host { + --wa-tooltip-background-color: var(--c-color-black-fill-loud); + --wa-tooltip-border-color: var(--c-color-black-border-loud); + --wa-tooltip-content-color: var(--c-color-black-on-loud); --wa-tooltip-padding: var( --c-tooltip-padding, calc(4rem / 16) calc(8rem / 16) ); --wa-tooltip-arrow-size: var(--c-tooltip-arrow-size, 5px); - --wa-tooltip-font-family: inherit; - --wa-tooltip-font-size: var( - --c-tooltip-font-size, - var(--c-text-base) - ); - --wa-tooltip-font-weight: var(--c-tooltip-font-weight, 400); - --wa-tooltip-line-height: var(--c-tooltip-line-height, 1.3); - --wa-tooltip-border-radius: var( - --c-tooltip-border-radius, - var(--c-radius-sm) - ); - font-weight: 400; - color: var(--c-tooltip-text, currentColor); + --wa-tooltip-font-family: var(--c-font-body); + --wa-tooltip-font-size: var(--c-text-base); + --wa-tooltip-font-weight: 400; + --wa-tooltip-line-height: 1.3; + --wa-tooltip-border-radius: var(--c-radius-sm); + } + + &::part(base) { box-shadow: var(--c-shadow-md); } + + .body { + color: var(--wa-tooltip-content-color); + font-weight: var(--wa-tooltip-font-weight); + font-family: var(--c-font-body); + } `, ]; } diff --git a/packages/craftcms-cp/src/components/visually-hidden/visually-hidden.ts b/packages/craftcms-cp/src/components/visually-hidden/visually-hidden.ts new file mode 100644 index 00000000000..3aca980a7bb --- /dev/null +++ b/packages/craftcms-cp/src/components/visually-hidden/visually-hidden.ts @@ -0,0 +1,33 @@ +import {css, html, LitElement} from 'lit'; +import CraftInfoIcon from '@src/components/info-icon/info-icon'; +import {property} from 'lit/decorators.js'; + +export default class CraftVisuallyHidden extends LitElement { + static override styles = css` + :host(:not([debug])) { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0 0 0 0); + clip-path: inset(50%); + white-space: nowrap; + } + `; + + @property({type: Boolean, reflect: true}) debug = false; + + protected override render(): unknown { + return html``; + } +} + +if (!customElements.get('craft-visually-hidden')) { + customElements.define('craft-visually-hidden', CraftVisuallyHidden); +} + +declare global { + interface HTMLElementTagNameMap { + 'craft-visually-hidden': CraftVisuallyHidden; + } +} diff --git a/packages/craftcms-cp/src/index.ts b/packages/craftcms-cp/src/index.ts index 787dc2239e3..36bc79c1bae 100644 --- a/packages/craftcms-cp/src/index.ts +++ b/packages/craftcms-cp/src/index.ts @@ -21,6 +21,7 @@ export {default as CraftSelect} from './components/select/select.js'; export {default as CraftOption} from './components/option/option.js'; export {default as CraftDropdown} from './components/dropdown/dropdown.js'; export {default as CraftIcon} from './components/icon/icon.js'; +export {default as CraftInfoIcon} from './components/info-icon/info-icon.js'; export {default as CraftTabs} from './components/tabs/tabs.js'; export {default as CraftCard} from './components/card/card.js'; export {default as CraftTab} from './components/tab/tab.js'; @@ -46,6 +47,7 @@ export {default as CraftProgress} from './components/progress/progress.js'; export {default as CraftProgressBar} from './components/progress-bar/progress-bar.js'; export {default as CraftRadioGroup} from './components/radio-group/radio-group.js'; export {default as CraftRadio} from './components/radio/radio.js'; +export {default as CraftVisuallyHidden} from './components/visually-hidden/visually-hidden.js'; /* plop:component */ export * from './utilities/cookies.js'; @@ -54,6 +56,7 @@ export * from './utilities/format.js'; export * from './utilities/api/actionClient.js'; export * from './utilities/api/apiClient.js'; export * from './utilities/string.js'; +export * from './utilities/dom.js'; // Services export {QueueService} from './services/Queue.js'; diff --git a/packages/craftcms-cp/src/services/AssetIndexer.ts b/packages/craftcms-cp/src/services/AssetIndexer.ts index 464d95db5fe..681c9eb0642 100644 --- a/packages/craftcms-cp/src/services/AssetIndexer.ts +++ b/packages/craftcms-cp/src/services/AssetIndexer.ts @@ -76,7 +76,7 @@ export interface StartIndexingParams { /** * Parameters for finishing an indexing session. */ -export interface FinishIndexingParams { +export interface FinishIndexingParams extends Record { sessionId: number; /** Folder IDs to delete */ deleteFolder?: Array; diff --git a/packages/craftcms-cp/src/styles/form.styles.ts b/packages/craftcms-cp/src/styles/form.styles.ts index 7bcfed5aa53..d2252c1b6ea 100644 --- a/packages/craftcms-cp/src/styles/form.styles.ts +++ b/packages/craftcms-cp/src/styles/form.styles.ts @@ -1,11 +1,16 @@ import {css} from 'lit'; export const baseInputStyles = css` + --_border-width: var( + --c-input-border-width, + var(--c-form-control-border-width) + ); + --_min-height: var(--c-input-height, var(--c-size-control-md)); font: inherit; color: var(--c-input-text, var(--c-text-default)); position: relative; - min-height: var(--c-input-height, var(--c-size-control-md)); - border-width: var(--c-input-border-width, var(--c-form-control-border-width)); + min-height: calc(var(--_min-height) - 2 * var(--_border-width)); + border-width: var(--_border-width); border-style: var(--c-input-border-style, var(--c-form-control-border-style)); border-color: var(--c-input-border-color, var(--c-form-control-border-color)); border-radius: var(--c-input-radius, var(--c-radius-sm)); diff --git a/packages/craftcms-cp/src/styles/shared/base.css b/packages/craftcms-cp/src/styles/shared/base.css index 927da4604ef..cedf3a922c2 100644 --- a/packages/craftcms-cp/src/styles/shared/base.css +++ b/packages/craftcms-cp/src/styles/shared/base.css @@ -40,21 +40,34 @@ ul { list-style: none; } -.code { - font-size: 0.9em; +.cp-code { + font-size: 0.75em; + font-family: var(--c-font-mono); display: inline-flex; padding: 0 var(--c-spacing-sm); - border: 1px solid rgba(0, 0, 0, 0.2); - background-color: rgba(0, 0, 0, 0.05); + color: var(--c-color-on-quiet); + border: 1px solid var(--c-color-border-quiet); + background-color: color-mix( + var(--c-color-fill-quiet) 90%, + var(--c-color-fill-loud) + ); border-radius: var(--c-radius-sm); } +.cp-icon { + width: 1em; + height: 1em; +} + hr { display: block; width: 100%; border: 0; - border-top: 1px solid var(--c-color-neutral-border-quiet); + height: 0; + border-block-start: 1px solid var(--c-color-neutral-border-quiet); margin-block: var(--c-spacing-lg); + margin-inline: 0; + color: transparent; } .index-grid { @@ -127,6 +140,9 @@ CP Table Applies some basic styling to table elements. +The idea here is that you can just apply `cp-table` to the parent table and have +most of the element display correctly which is why we don't do BEM or anything here + Modify with `cp-table--compact` to reduce the vertical cell padding Modify with `cp-table--borderless` to remove the inline cell padding Modify with `cp-table--auto` to apply table-layout: auto @@ -134,6 +150,8 @@ Modify with `cp-table--auto` to apply table-layout: auto .cp-table { --_cell-spacing-inline: var(--c-spacing-md); --_cell-spacing-block: var(--c-spacing-md); + --table-column-count: auto-fill; + --table-template-columns: repeat(var(--table-column-count), 1fr); text-align: left; width: 100%; border-spacing: 0; @@ -143,34 +161,36 @@ Modify with `cp-table--auto` to apply table-layout: auto table-layout: fixed; } + tr { + display: grid; + grid-template-columns: var(--table-template-columns); + grid-template-rows: minmax(var(--c-size-control-md), auto); + } + thead, th { background-color: var(--c-color-neutral-fill-quiet); } - .cell { - padding: 0; - vertical-align: middle; + th, + td { + display: flex; + align-items: start; + flex-direction: column; + justify-content: center; + padding-block: var(--_cell-spacing-block); padding-inline: var(--_cell-spacing-inline); - padding-block: 0; position: relative; - height: var(--c-size-control-md); - &:has(textarea), - &:has(input:not([type='checkbox']):not([type='radio'])) { + &:has(.cp-table-input:not([type='checkbox']):not([type='radio'])) { padding: 0; } } td, - th, - tr:not(:last-child) { - border-block-end: 1px solid var(--c-color-neutral-border-quiet); - } - - td:not(:first-child), - th:not(:first-child) { - border-inline-start: 1px solid var(--c-color-neutral-border-quiet); + th { + border-block-end: 1px solid + color-mix(var(--c-color-neutral-border-quiet) 60%, transparent); } textarea, @@ -189,6 +209,13 @@ Modify with `cp-table--auto` to apply table-layout: auto } } +.cp-table--ruled { + td:not(:first-child), + th:not(:first-child) { + border-inline-start: 1px solid var(--c-color-neutral-border-quiet); + } +} + .cp-table--padded { th, td { @@ -213,6 +240,10 @@ Modify with `cp-table--auto` to apply table-layout: auto --_cell-spacing-block: var(--c-spacing-sm); } +.cp-table--spacious { + --_cell-spacing-block: var(--c-spacing-lg); +} + .cp-table-header, .cp-table-footer { padding: var(--c-spacing-md); diff --git a/packages/craftcms-cp/src/styles/shared/colorable.css b/packages/craftcms-cp/src/styles/shared/colorable.css index 7e82e67bb61..2b60e1d8cbd 100644 --- a/packages/craftcms-cp/src/styles/shared/colorable.css +++ b/packages/craftcms-cp/src/styles/shared/colorable.css @@ -1,4 +1,4 @@ -/* Auto-generated by scripts/generate-colors.ts — do not edit manually */ +/* Auto-generated by scripts/generate-colors.js — do not edit manually */ :root { /* red */ @@ -215,8 +215,8 @@ --c-color-black-fill-normal: var(--color-gray-900); --c-color-black-fill-loud: var(--color-gray-900); --c-color-black-border-quiet: var(--color-gray-800); - --c-color-black-border-normal: undefined; - --c-color-black-border-loud: undefined; + --c-color-black-border-normal: var(--color-gray-800); + --c-color-black-border-loud: var(--color-gray-800); --c-color-black-on-quiet: var(--color-gray-100); --c-color-black-on-normal: var(--color-gray-100); --c-color-black-on-loud: var(--color-gray-100); @@ -444,24 +444,7 @@ --c-color-black-on-loud: var(--color-gray-300); } -.c-colorable, -[data-color] { - --c-color-fill-quiet: var(--c-color-neutral-fill-quiet); - --c-color-fill-normal: var(--c-color-neutral-fill-quiet); - --c-color-fill-loud: var(--c-color-neutral-fill-quiet); - --c-color-border-quiet: var(--c-color-neutral-border-quiet); - --c-color-border-normal: var(--c-color-neutral-border-quiet); - --c-color-border-loud: var(--c-color-neutral-border-quiet); - --c-color-on-quiet: var(--c-color-neutral-on-quiet); - --c-color-on-normal: var(--c-color-neutral-on-quiet); - --c-color-on-loud: var(--c-color-neutral-on-quiet); - - background-color: var(--c-color-fill-quiet); - border-color: var(--c-color-border-quiet); - color: var(--c-color-on-quiet); -} - -.c-colorable--red, +.cp-color-red, [data-color='red'] { --c-color-fill-quiet: var(--c-color-red-fill-quiet); --c-color-border-quiet: var(--c-color-red-border-quiet); @@ -473,7 +456,7 @@ --c-color-border-loud: var(--c-color-red-border-loud); --c-color-on-loud: var(--c-color-red-on-loud); } -.c-colorable--orange, +.cp-color-orange, [data-color='orange'] { --c-color-fill-quiet: var(--c-color-orange-fill-quiet); --c-color-border-quiet: var(--c-color-orange-border-quiet); @@ -485,7 +468,7 @@ --c-color-border-loud: var(--c-color-orange-border-loud); --c-color-on-loud: var(--c-color-orange-on-loud); } -.c-colorable--amber, +.cp-color-amber, [data-color='amber'] { --c-color-fill-quiet: var(--c-color-amber-fill-quiet); --c-color-border-quiet: var(--c-color-amber-border-quiet); @@ -497,7 +480,7 @@ --c-color-border-loud: var(--c-color-amber-border-loud); --c-color-on-loud: var(--c-color-amber-on-loud); } -.c-colorable--yellow, +.cp-color-yellow, [data-color='yellow'] { --c-color-fill-quiet: var(--c-color-yellow-fill-quiet); --c-color-border-quiet: var(--c-color-yellow-border-quiet); @@ -509,7 +492,7 @@ --c-color-border-loud: var(--c-color-yellow-border-loud); --c-color-on-loud: var(--c-color-yellow-on-loud); } -.c-colorable--lime, +.cp-color-lime, [data-color='lime'] { --c-color-fill-quiet: var(--c-color-lime-fill-quiet); --c-color-border-quiet: var(--c-color-lime-border-quiet); @@ -521,7 +504,7 @@ --c-color-border-loud: var(--c-color-lime-border-loud); --c-color-on-loud: var(--c-color-lime-on-loud); } -.c-colorable--green, +.cp-color-green, [data-color='green'] { --c-color-fill-quiet: var(--c-color-green-fill-quiet); --c-color-border-quiet: var(--c-color-green-border-quiet); @@ -533,7 +516,7 @@ --c-color-border-loud: var(--c-color-green-border-loud); --c-color-on-loud: var(--c-color-green-on-loud); } -.c-colorable--emerald, +.cp-color-emerald, [data-color='emerald'] { --c-color-fill-quiet: var(--c-color-emerald-fill-quiet); --c-color-border-quiet: var(--c-color-emerald-border-quiet); @@ -545,7 +528,7 @@ --c-color-border-loud: var(--c-color-emerald-border-loud); --c-color-on-loud: var(--c-color-emerald-on-loud); } -.c-colorable--teal, +.cp-color-teal, [data-color='teal'] { --c-color-fill-quiet: var(--c-color-teal-fill-quiet); --c-color-border-quiet: var(--c-color-teal-border-quiet); @@ -557,7 +540,7 @@ --c-color-border-loud: var(--c-color-teal-border-loud); --c-color-on-loud: var(--c-color-teal-on-loud); } -.c-colorable--cyan, +.cp-color-cyan, [data-color='cyan'] { --c-color-fill-quiet: var(--c-color-cyan-fill-quiet); --c-color-border-quiet: var(--c-color-cyan-border-quiet); @@ -569,7 +552,7 @@ --c-color-border-loud: var(--c-color-cyan-border-loud); --c-color-on-loud: var(--c-color-cyan-on-loud); } -.c-colorable--sky, +.cp-color-sky, [data-color='sky'] { --c-color-fill-quiet: var(--c-color-sky-fill-quiet); --c-color-border-quiet: var(--c-color-sky-border-quiet); @@ -581,7 +564,7 @@ --c-color-border-loud: var(--c-color-sky-border-loud); --c-color-on-loud: var(--c-color-sky-on-loud); } -.c-colorable--blue, +.cp-color-blue, [data-color='blue'] { --c-color-fill-quiet: var(--c-color-blue-fill-quiet); --c-color-border-quiet: var(--c-color-blue-border-quiet); @@ -593,7 +576,7 @@ --c-color-border-loud: var(--c-color-blue-border-loud); --c-color-on-loud: var(--c-color-blue-on-loud); } -.c-colorable--indigo, +.cp-color-indigo, [data-color='indigo'] { --c-color-fill-quiet: var(--c-color-indigo-fill-quiet); --c-color-border-quiet: var(--c-color-indigo-border-quiet); @@ -605,7 +588,7 @@ --c-color-border-loud: var(--c-color-indigo-border-loud); --c-color-on-loud: var(--c-color-indigo-on-loud); } -.c-colorable--violet, +.cp-color-violet, [data-color='violet'] { --c-color-fill-quiet: var(--c-color-violet-fill-quiet); --c-color-border-quiet: var(--c-color-violet-border-quiet); @@ -617,7 +600,7 @@ --c-color-border-loud: var(--c-color-violet-border-loud); --c-color-on-loud: var(--c-color-violet-on-loud); } -.c-colorable--purple, +.cp-color-purple, [data-color='purple'] { --c-color-fill-quiet: var(--c-color-purple-fill-quiet); --c-color-border-quiet: var(--c-color-purple-border-quiet); @@ -629,7 +612,7 @@ --c-color-border-loud: var(--c-color-purple-border-loud); --c-color-on-loud: var(--c-color-purple-on-loud); } -.c-colorable--fuchsia, +.cp-color-fuchsia, [data-color='fuchsia'] { --c-color-fill-quiet: var(--c-color-fuchsia-fill-quiet); --c-color-border-quiet: var(--c-color-fuchsia-border-quiet); @@ -641,7 +624,7 @@ --c-color-border-loud: var(--c-color-fuchsia-border-loud); --c-color-on-loud: var(--c-color-fuchsia-on-loud); } -.c-colorable--pink, +.cp-color-pink, [data-color='pink'] { --c-color-fill-quiet: var(--c-color-pink-fill-quiet); --c-color-border-quiet: var(--c-color-pink-border-quiet); @@ -653,7 +636,7 @@ --c-color-border-loud: var(--c-color-pink-border-loud); --c-color-on-loud: var(--c-color-pink-on-loud); } -.c-colorable--rose, +.cp-color-rose, [data-color='rose'] { --c-color-fill-quiet: var(--c-color-rose-fill-quiet); --c-color-border-quiet: var(--c-color-rose-border-quiet); @@ -665,7 +648,7 @@ --c-color-border-loud: var(--c-color-rose-border-loud); --c-color-on-loud: var(--c-color-rose-on-loud); } -.c-colorable--white, +.cp-color-white, [data-color='white'] { --c-color-fill-quiet: var(--c-color-white-fill-quiet); --c-color-border-quiet: var(--c-color-white-border-quiet); @@ -677,7 +660,7 @@ --c-color-border-loud: var(--c-color-white-border-loud); --c-color-on-loud: var(--c-color-white-on-loud); } -.c-colorable--gray, +.cp-color-gray, [data-color='gray'] { --c-color-fill-quiet: var(--c-color-gray-fill-quiet); --c-color-border-quiet: var(--c-color-gray-border-quiet); @@ -689,7 +672,7 @@ --c-color-border-loud: var(--c-color-gray-border-loud); --c-color-on-loud: var(--c-color-gray-on-loud); } -.c-colorable--black, +.cp-color-black, [data-color='black'] { --c-color-fill-quiet: var(--c-color-black-fill-quiet); --c-color-border-quiet: var(--c-color-black-border-quiet); @@ -701,3 +684,87 @@ --c-color-border-loud: var(--c-color-black-border-loud); --c-color-on-loud: var(--c-color-black-on-loud); } +.cp-color-neutral, +[data-color='neutral'] { + --c-color-fill-quiet: var(--c-color-neutral-fill-quiet); + --c-color-border-quiet: var(--c-color-neutral-border-quiet); + --c-color-on-quiet: var(--c-color-neutral-on-quiet); + --c-color-fill-normal: var(--c-color-neutral-fill-normal); + --c-color-border-normal: var(--c-color-neutral-border-normal); + --c-color-on-normal: var(--c-color-neutral-on-normal); + --c-color-fill-loud: var(--c-color-neutral-fill-loud); + --c-color-border-loud: var(--c-color-neutral-border-loud); + --c-color-on-loud: var(--c-color-neutral-on-loud); +} +.cp-color-brand, +[data-color='brand'] { + --c-color-fill-quiet: var(--c-color-brand-fill-quiet); + --c-color-border-quiet: var(--c-color-brand-border-quiet); + --c-color-on-quiet: var(--c-color-brand-on-quiet); + --c-color-fill-normal: var(--c-color-brand-fill-normal); + --c-color-border-normal: var(--c-color-brand-border-normal); + --c-color-on-normal: var(--c-color-brand-on-normal); + --c-color-fill-loud: var(--c-color-brand-fill-loud); + --c-color-border-loud: var(--c-color-brand-border-loud); + --c-color-on-loud: var(--c-color-brand-on-loud); +} +.cp-color-accent, +[data-color='accent'] { + --c-color-fill-quiet: var(--c-color-accent-fill-quiet); + --c-color-border-quiet: var(--c-color-accent-border-quiet); + --c-color-on-quiet: var(--c-color-accent-on-quiet); + --c-color-fill-normal: var(--c-color-accent-fill-normal); + --c-color-border-normal: var(--c-color-accent-border-normal); + --c-color-on-normal: var(--c-color-accent-on-normal); + --c-color-fill-loud: var(--c-color-accent-fill-loud); + --c-color-border-loud: var(--c-color-accent-border-loud); + --c-color-on-loud: var(--c-color-accent-on-loud); +} +.cp-color-info, +[data-color='info'] { + --c-color-fill-quiet: var(--c-color-info-fill-quiet); + --c-color-border-quiet: var(--c-color-info-border-quiet); + --c-color-on-quiet: var(--c-color-info-on-quiet); + --c-color-fill-normal: var(--c-color-info-fill-normal); + --c-color-border-normal: var(--c-color-info-border-normal); + --c-color-on-normal: var(--c-color-info-on-normal); + --c-color-fill-loud: var(--c-color-info-fill-loud); + --c-color-border-loud: var(--c-color-info-border-loud); + --c-color-on-loud: var(--c-color-info-on-loud); +} +.cp-color-success, +[data-color='success'] { + --c-color-fill-quiet: var(--c-color-success-fill-quiet); + --c-color-border-quiet: var(--c-color-success-border-quiet); + --c-color-on-quiet: var(--c-color-success-on-quiet); + --c-color-fill-normal: var(--c-color-success-fill-normal); + --c-color-border-normal: var(--c-color-success-border-normal); + --c-color-on-normal: var(--c-color-success-on-normal); + --c-color-fill-loud: var(--c-color-success-fill-loud); + --c-color-border-loud: var(--c-color-success-border-loud); + --c-color-on-loud: var(--c-color-success-on-loud); +} +.cp-color-warning, +[data-color='warning'] { + --c-color-fill-quiet: var(--c-color-warning-fill-quiet); + --c-color-border-quiet: var(--c-color-warning-border-quiet); + --c-color-on-quiet: var(--c-color-warning-on-quiet); + --c-color-fill-normal: var(--c-color-warning-fill-normal); + --c-color-border-normal: var(--c-color-warning-border-normal); + --c-color-on-normal: var(--c-color-warning-on-normal); + --c-color-fill-loud: var(--c-color-warning-fill-loud); + --c-color-border-loud: var(--c-color-warning-border-loud); + --c-color-on-loud: var(--c-color-warning-on-loud); +} +.cp-color-danger, +[data-color='danger'] { + --c-color-fill-quiet: var(--c-color-danger-fill-quiet); + --c-color-border-quiet: var(--c-color-danger-border-quiet); + --c-color-on-quiet: var(--c-color-danger-on-quiet); + --c-color-fill-normal: var(--c-color-danger-fill-normal); + --c-color-border-normal: var(--c-color-danger-border-normal); + --c-color-on-normal: var(--c-color-danger-on-normal); + --c-color-fill-loud: var(--c-color-danger-fill-loud); + --c-color-border-loud: var(--c-color-danger-border-loud); + --c-color-on-loud: var(--c-color-danger-on-loud); +} diff --git a/packages/craftcms-cp/src/styles/shared/tokens.css b/packages/craftcms-cp/src/styles/shared/tokens.css index 39c24cb3697..409c8a93059 100644 --- a/packages/craftcms-cp/src/styles/shared/tokens.css +++ b/packages/craftcms-cp/src/styles/shared/tokens.css @@ -137,6 +137,7 @@ --c-size-icon-lg: calc(22rem / 16); --c-size-icon-xl: calc(30rem / 16); + --c-size-control-2xs: calc(14rem / 16); --c-size-control-xs: calc(16rem / 16); --c-size-control-sm: calc(24rem / 16); --c-size-control-md: calc(34rem / 16); diff --git a/packages/craftcms-cp/src/utilities/dom.test.ts b/packages/craftcms-cp/src/utilities/dom.test.ts new file mode 100644 index 00000000000..aceeca14a8c --- /dev/null +++ b/packages/craftcms-cp/src/utilities/dom.test.ts @@ -0,0 +1,165 @@ +import {describe, test, expect, beforeEach, vi} from 'vitest'; + +// Prevent happy-dom from making real network requests for CSS/JS files +const happyDOM = (window as any).happyDOM; +if (happyDOM?.settings) { + happyDOM.settings.disableCSSFileLoading = true; + happyDOM.settings.disableJavaScriptFileLoading = true; +} + +// Helper to get a fresh module instance (resets cached existingCss/existingJs) +async function freshImport() { + vi.resetModules(); + return await import('./dom.js'); +} + +describe('appendHeadHtml', () => { + beforeEach(() => { + document.head.innerHTML = ''; + document.body.innerHTML = ''; + }); + + test('does nothing for empty string', async () => { + const {appendHeadHtml} = await freshImport(); + await appendHeadHtml(''); + expect(document.head.children.length).toBe(0); + }); + + test('appends a link element to head', async () => { + const {appendHeadHtml} = await freshImport(); + await appendHeadHtml( + '' + ); + const links = document.head.querySelectorAll('link'); + expect(links.length).toBe(1); + expect(links[0].getAttribute('rel')).toBe('stylesheet'); + expect(links[0].getAttribute('href')).toBe('https://example.com/style.css'); + }); + + test('appends a script element to head', async () => { + const {appendHeadHtml} = await freshImport(); + await appendHeadHtml( + '' + ); + const scripts = document.head.querySelectorAll('script'); + expect(scripts.length).toBe(1); + expect(scripts[0].getAttribute('src')).toBe( + 'https://example.com/script.js' + ); + }); + + test('appends an inline script to head', async () => { + const {appendHeadHtml} = await freshImport(); + await appendHeadHtml(''); + const scripts = document.head.querySelectorAll('script'); + expect(scripts.length).toBe(1); + expect(scripts[0].textContent).toBe('console.log("hello")'); + }); + + test('appends arbitrary HTML to head', async () => { + const {appendHeadHtml} = await freshImport(); + await appendHeadHtml(''); + const metas = document.head.querySelectorAll('meta[name="description"]'); + expect(metas.length).toBe(1); + expect(metas[0].getAttribute('content')).toBe('test'); + }); + + test('preserves link attributes when appending', async () => { + const {appendHeadHtml} = await freshImport(); + await appendHeadHtml( + '' + ); + const link = document.head.querySelector('link')!; + expect(link.getAttribute('rel')).toBe('stylesheet'); + expect(link.getAttribute('href')).toBe('https://example.com/a.css'); + expect(link.getAttribute('media')).toBe('print'); + expect(link.getAttribute('crossorigin')).toBe('anonymous'); + }); + + test('preserves script attributes when appending', async () => { + const {appendHeadHtml} = await freshImport(); + await appendHeadHtml( + '' + ); + const script = document.head.querySelector('script')!; + expect(script.getAttribute('src')).toBe('https://example.com/b.js'); + expect(script.getAttribute('type')).toBe('module'); + expect(script.hasAttribute('defer')).toBe(true); + }); +}); + +describe('appendBodyHtml', () => { + beforeEach(() => { + document.head.innerHTML = ''; + document.body.innerHTML = ''; + }); + + test('appends elements to body', async () => { + const {appendBodyHtml} = await freshImport(); + await appendBodyHtml('
Hello
'); + const div = document.body.querySelector('#test-div'); + expect(div).not.toBeNull(); + expect(div!.textContent).toBe('Hello'); + }); + + test('appends script with src to body', async () => { + const {appendBodyHtml} = await freshImport(); + await appendBodyHtml(''); + const scripts = document.body.querySelectorAll('script'); + expect(scripts.length).toBe(1); + expect(scripts[0].getAttribute('src')).toBe('https://example.com/body.js'); + }); +}); + +describe('CSS deduplication', () => { + beforeEach(() => { + document.head.innerHTML = ''; + document.body.innerHTML = ''; + }); + + test('does not add duplicate CSS links', async () => { + const {appendHeadHtml} = await freshImport(); + const css = ''; + await appendHeadHtml(css); + await appendHeadHtml(css); + const links = document.head.querySelectorAll('link'); + expect(links.length).toBe(1); + }); + + test('adds different CSS links', async () => { + const {appendHeadHtml} = await freshImport(); + await appendHeadHtml( + '' + ); + await appendHeadHtml( + '' + ); + const links = document.head.querySelectorAll('link'); + expect(links.length).toBe(2); + }); +}); + +describe('JS deduplication', () => { + beforeEach(() => { + document.head.innerHTML = ''; + document.body.innerHTML = ''; + }); + + test('does not add duplicate script src', async () => { + const {appendBodyHtml} = await freshImport(); + const js = ''; + await appendBodyHtml(js); + await appendBodyHtml(js); + const scripts = document.body.querySelectorAll('script'); + expect(scripts.length).toBe(1); + }); + + test('inline scripts are always added (no deduplication)', async () => { + const {appendBodyHtml} = await freshImport(); + const inline = ''; + await appendBodyHtml(inline); + await appendBodyHtml(inline); + const scripts = document.body.querySelectorAll('script'); + expect(scripts.length).toBe(2); + }); +}); diff --git a/packages/craftcms-cp/src/utilities/dom.ts b/packages/craftcms-cp/src/utilities/dom.ts new file mode 100644 index 00000000000..a493472dd93 --- /dev/null +++ b/packages/craftcms-cp/src/utilities/dom.ts @@ -0,0 +1,79 @@ +let existingCss: string[] | null = null; +let existingJs: string[] | null = null; + +async function appendHtml(html: string, parent: HTMLElement): Promise { + if (!html) { + return; + } + + const div = document.createElement('div'); + div.innerHTML = html.trim(); + const nodes = Array.from(div.childNodes); + + for (const node of nodes) { + if (node instanceof HTMLLinkElement && node.href) { + if (!existingCss) { + existingCss = Array.from(document.querySelectorAll('link[href]')).map( + (n) => (n as HTMLLinkElement).href.replace(/&/g, '&') + ); + } + + const href = node.href.replace(/&/g, '&'); + if (existingCss.includes(href)) { + continue; + } + + existingCss.push(href); + const link = document.createElement('link'); + Array.from(node.attributes).forEach((attr) => { + link.setAttribute(attr.name, attr.value); + }); + parent.appendChild(link); + continue; + } + + if (node instanceof HTMLScriptElement) { + const script = document.createElement('script'); + Array.from(node.attributes).forEach((attr) => { + script.setAttribute(attr.name, attr.value); + }); + + if (node.src) { + if (!existingJs) { + existingJs = Array.from(document.querySelectorAll('script[src]')).map( + (n) => (n as HTMLScriptElement).src.replace(/&/g, '&') + ); + } + + const src = node.src.replace(/&/g, '&'); + if (existingJs.includes(src)) { + continue; + } + + existingJs.push(src); + script.async = false; + } else { + script.textContent = node.textContent; + } + + parent.appendChild(script); + continue; + } + + parent.appendChild(node.cloneNode(true)); + } +} + +/** + * Appends HTML to the page ``. + */ +export async function appendHeadHtml(html: string): Promise { + await appendHtml(html, document.head); +} + +/** + * Appends HTML to the page ``. + */ +export async function appendBodyHtml(html: string): Promise { + await appendHtml(html, document.body); +} diff --git a/packages/craftcms-cp/src/utilities/icons.ts b/packages/craftcms-cp/src/utilities/icons.ts index ac45b59a3d9..bcd65d2df8c 100644 --- a/packages/craftcms-cp/src/utilities/icons.ts +++ b/packages/craftcms-cp/src/utilities/icons.ts @@ -99,7 +99,7 @@ export function getIconUrl( folder = 'brands'; } - if (resolvedVariant === 'custom-icons') { + if (family === 'custom-icons' || resolvedVariant === 'custom-icons') { folder = 'custom-icons'; } diff --git a/packages/craftcms-cp/src/utilities/string.ts b/packages/craftcms-cp/src/utilities/string.ts index e39f2561243..8cd7f427f85 100644 --- a/packages/craftcms-cp/src/utilities/string.ts +++ b/packages/craftcms-cp/src/utilities/string.ts @@ -1609,3 +1609,7 @@ export function toUriFormat(value: string): string { return words.join('-'); } + +export function capitalize(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); +} diff --git a/packages/craftcms-legacy/.stylelintrc.json b/packages/craftcms-legacy/.stylelintrc.json index b3c44260f12..aeb14ad2519 100644 --- a/packages/craftcms-legacy/.stylelintrc.json +++ b/packages/craftcms-legacy/.stylelintrc.json @@ -8,12 +8,7 @@ "declaration-empty-line-before": null, "no-descending-specificity": null, "no-duplicate-selectors": null, - "no-invalid-position-at-import-rule": [ - true, - { - "ignoreAtRules": ["tailwind", "use"] - } - ], + "no-invalid-position-at-import-rule": null, "selector-class-pattern": null, "liberty/use-logical-spec": [ "always", diff --git a/packages/craftcms-legacy/cp/src/Craft.js b/packages/craftcms-legacy/cp/src/Craft.js index ca50ff3b6a4..46de65e8d6c 100644 --- a/packages/craftcms-legacy/cp/src/Craft.js +++ b/packages/craftcms-legacy/cp/src/Craft.js @@ -110,8 +110,6 @@ import './js/UserIndex.js'; // Custom elements import './js/CraftGlobalSidebar.js'; import './js/CraftDisclosure.js'; -import './js/CraftSpinner.js'; import './js/CraftTooltip.js'; import './js/CraftElementLabel'; import './js/CraftProxyScrollbar'; -import './js/CraftCopyAttribute.js'; diff --git a/packages/craftcms-legacy/cp/src/css/_color-palette.scss b/packages/craftcms-legacy/cp/src/css/_color-palette.scss deleted file mode 100644 index 66c47fa4ac9..00000000000 --- a/packages/craftcms-legacy/cp/src/css/_color-palette.scss +++ /dev/null @@ -1,224 +0,0 @@ -@charset "UTF-8"; -@use '@craftcms/sass/mixins'; - -:root { - --white: #fff; - --black: #000; - --gray-050-hsl: 212, 60%, 97%; - --gray-100-hsl: 212, 50%, 93%; - --gray-150-hsl: 212, 40%, 89%; - --gray-200-hsl: 212, 30%, 85%; - --gray-300-hsl: 211, 13%, 65%; - --gray-350-hsl: 211, 11%, 59%; - --gray-400-hsl: 210, 10%, 53%; - --gray-500-hsl: 211, 12%, 43%; - --gray-550-hsl: 210, 13%, 40%; - --gray-600-hsl: 209, 14%, 37%; - --gray-700-hsl: 209, 18%, 30%; - --gray-800-hsl: 209, 20%, 25%; - --gray-900-hsl: 210, 24%, 16%; - --gray-1000-hsl: 210, 24%, 10%; - --gray-050: hsl(var(--gray-050-hsl)); - --gray-100: hsl(var(--gray-100-hsl)); - --gray-150: hsl(var(--gray-150-hsl)); - --gray-200: hsl(var(--gray-200-hsl)); - --gray-300: hsl(var(--gray-300-hsl)); - --gray-350: hsl(var(--gray-350-hsl)); - --gray-400: hsl(var(--gray-400-hsl)); - --gray-500: hsl(var(--gray-500-hsl)); - --gray-550: hsl(var(--gray-550-hsl)); - --gray-600: hsl(var(--gray-600-hsl)); - --gray-700: hsl(var(--gray-700-hsl)); - --gray-800: hsl(var(--gray-800-hsl)); - --gray-900: hsl(var(--gray-900-hsl)); - --gray-1000: hsl(var(--gray-1000-hsl)); - --red-050: #fef2f2; - --red-100: #fee2e2; - --red-200: #fecaca; - --red-300: #fca5a5; - --red-400: #f87171; - --red-500: #ef4444; - --red-600: #dc2626; - --red-700: #b91c1c; - --red-800: #991b1b; - --red-900: #7f1d1d; - --red-950: #450a0a; - --orange-050: #fff7ed; - --orange-100: #ffedd5; - --orange-200: #fed7aa; - --orange-300: #fdba74; - --orange-400: #fb923c; - --orange-500: #f97316; - --orange-600: #ea580c; - --orange-700: #c2410c; - --orange-800: #9a3412; - --orange-900: #7c2d12; - --orange-950: #431407; - --amber-050: #fffbeb; - --amber-100: #fef3c7; - --amber-200: #fde68a; - --amber-300: #fcd34d; - --amber-400: #fbbf24; - --amber-500: #f59e0b; - --amber-600: #d97706; - --amber-700: #b45309; - --amber-800: #92400e; - --amber-900: #78350f; - --amber-950: #451a03; - --yellow-050: #fefce8; - --yellow-100: #fef9c3; - --yellow-200: #fef08a; - --yellow-300: #fde047; - --yellow-400: #facc15; - --yellow-500: #eab308; - --yellow-600: #ca8a04; - --yellow-700: #a16207; - --yellow-750: #93580b; - --yellow-800: #854d0e; - --yellow-900: #713f12; - --yellow-950: #422006; - --lime-050: #f7fee7; - --lime-100: #ecfccb; - --lime-200: #d9f99d; - --lime-300: #bef264; - --lime-400: #a3e635; - --lime-500: #84cc16; - --lime-600: #65a30d; - --lime-700: #4d7c0f; - --lime-800: #3f6212; - --lime-900: #365314; - --lime-950: #1a2e05; - --green-050: #f0fdf4; - --green-100: #dcfce7; - --green-200: #bbf7d0; - --green-300: #86efac; - --green-400: #4ade80; - --green-500: #22c55e; - --green-600: #16a34a; - --green-700: #15803d; - --green-800: #166534; - --green-900: #14532d; - --green-950: #052e16; - --emerald-050: #ecfdf5; - --emerald-100: #d1fae5; - --emerald-200: #a7f3d0; - --emerald-300: #6ee7b7; - --emerald-400: #34d399; - --emerald-500: #10b981; - --emerald-600: #059669; - --emerald-700: #047857; - --emerald-800: #065f46; - --emerald-900: #064e3b; - --emerald-950: #022c22; - --teal-050: #f0fdfa; - --teal-100: #ccfbf1; - --teal-200: #99f6e4; - --teal-300: #5eead4; - --teal-400: #2dd4bf; - --teal-500: #14b8a6; - --teal-550: #11a697; - --teal-600: #0d9488; - --teal-700: #0f766e; - --teal-800: #115e59; - --teal-900: #134e4a; - --teal-950: #042f2e; - --cyan-050: #ecfeff; - --cyan-100: #cffafe; - --cyan-200: #a5f3fc; - --cyan-300: #67e8f9; - --cyan-400: #22d3ee; - --cyan-500: #06b6d4; - --cyan-600: #0891b2; - --cyan-700: #0e7490; - --cyan-800: #155e75; - --cyan-900: #164e63; - --cyan-950: #083344; - --sky-050: #f0f9ff; - --sky-100: #e0f2fe; - --sky-200: #bae6fd; - --sky-300: #7dd3fc; - --sky-400: #38bdf8; - --sky-500: #0ea5e9; - --sky-600: #0284c7; - --sky-700: #0369a1; - --sky-800: #075985; - --sky-900: #0c4a6e; - --sky-950: #082f49; - --blue-050: #eff6ff; - --blue-100: #dbeafe; - --blue-200: #bfdbfe; - --blue-300: #93c5fd; - --blue-400: #60a5fa; - --blue-500: #3b82f6; - --blue-600: #2563eb; - --blue-700: #1d4ed8; - --blue-800: #1e40af; - --blue-900: #1e3a8a; - --blue-950: #172554; - --indigo-050: #eef2ff; - --indigo-100: #e0e7ff; - --indigo-200: #c7d2fe; - --indigo-300: #a5b4fc; - --indigo-400: #818cf8; - --indigo-500: #6366f1; - --indigo-600: #4f46e5; - --indigo-700: #4338ca; - --indigo-800: #3730a3; - --indigo-900: #312e81; - --indigo-950: #1e1b4b; - --violet-050: #f5f3ff; - --violet-100: #ede9fe; - --violet-200: #ddd6fe; - --violet-300: #c4b5fd; - --violet-400: #a78bfa; - --violet-500: #8b5cf6; - --violet-600: #7c3aed; - --violet-700: #6d28d9; - --violet-800: #5b21b6; - --violet-900: #4c1d95; - --violet-950: #2e1065; - --purple-050: #faf5ff; - --purple-100: #f3e8ff; - --purple-200: #e9d5ff; - --purple-300: #d8b4fe; - --purple-400: #c084fc; - --purple-500: #a855f7; - --purple-600: #9333ea; - --purple-700: #7e22ce; - --purple-800: #6b21a8; - --purple-900: #581c87; - --purple-950: #3b0764; - --fuchsia-050: #fdf4ff; - --fuchsia-100: #fae8ff; - --fuchsia-200: #f5d0fe; - --fuchsia-300: #f0abfc; - --fuchsia-400: #e879f9; - --fuchsia-500: #d946ef; - --fuchsia-600: #c026d3; - --fuchsia-700: #a21caf; - --fuchsia-800: #86198f; - --fuchsia-900: #701a75; - --fuchsia-950: #4a044e; - --pink-050: #fdf2f8; - --pink-100: #fce7f3; - --pink-200: #fbcfe8; - --pink-300: #f9a8d4; - --pink-400: #f472b6; - --pink-500: #ec4899; - --pink-600: #db2777; - --pink-700: #be185d; - --pink-800: #9d174d; - --pink-900: #831843; - --pink-950: #500724; - --rose-050: #fff1f2; - --rose-100: #ffe4e6; - --rose-200: #fecdd3; - --rose-300: #fda4af; - --rose-400: #fb7185; - --rose-500: #f43f5e; - --rose-600: #e11d48; - --rose-700: #be123c; - --rose-800: #9f1239; - --rose-900: #881337; - --rose-950: #4c0519; -} diff --git a/packages/craftcms-legacy/cp/src/css/_compat.scss b/packages/craftcms-legacy/cp/src/css/_compat.scss new file mode 100644 index 00000000000..c35d37009b3 --- /dev/null +++ b/packages/craftcms-legacy/cp/src/css/_compat.scss @@ -0,0 +1,622 @@ +/** + * Legacy Token Mapping + * + * This file maps legacy CSS variables to the new token system defined in + * @craftcms/cp tokens.css. Import this file to provide backwards compatibility + * for legacy styles. + * + * Variables mapped to --CHANGE require manual review and update. + */ + +:root { + /** + * Placeholder for variables that need manual review + */ + --CHANGE: red; + + /** + * Color Palette Mapping + * Legacy colors (--{color}-{number}) to new format (--color-{color}-{number}) + */ + + /* White/Black */ + --white: var(--color-white, #fff); + --black: var(--color-black, #000); + + /* Gray */ + --gray-050: var(--color-gray-50); + --gray-100: var(--color-gray-100); + --gray-150: var(--color-gray-150); + --gray-200: var(--color-gray-200); + --gray-300: var(--color-gray-300); + --gray-350: var(--color-gray-350); + --gray-400: var(--color-gray-400); + --gray-500: var(--color-gray-500); + --gray-550: var(--color-gray-550); + --gray-600: var(--color-gray-600); + --gray-700: var(--color-gray-700); + --gray-800: var(--color-gray-800); + --gray-900: var(--color-gray-900); + --gray-1000: var(--color-gray-1000); + + /* Gray HSL versions */ + --gray-050-hsl: var(--color-gray-50-hsl); + --gray-100-hsl: var(--color-gray-100-hsl); + --gray-150-hsl: var(--color-gray-150-hsl); + --gray-200-hsl: var(--color-gray-200-hsl); + --gray-300-hsl: var(--color-gray-300-hsl); + --gray-350-hsl: var(--color-gray-350-hsl); + --gray-400-hsl: var(--color-gray-400-hsl); + --gray-500-hsl: var(--color-gray-500-hsl); + --gray-550-hsl: var(--color-gray-550-hsl); + --gray-600-hsl: var(--color-gray-600-hsl); + --gray-700-hsl: var(--color-gray-700-hsl); + --gray-800-hsl: var(--color-gray-800-hsl); + --gray-900-hsl: var(--color-gray-900-hsl); + --gray-1000-hsl: var(--color-gray-1000-hsl); + + /* Red - direct mapping (Tailwind red scale) */ + --red-050: var(--color-red-50); + --red-100: var(--color-red-100); + --red-200: var(--color-red-200); + --red-300: var(--color-red-300); + --red-400: var(--color-red-400); + --red-500: var(--color-red-500); + --red-600: var(--color-red-600); + --red-700: var(--color-red-700); + --red-800: var(--color-red-800); + --red-900: var(--color-red-900); + --red-950: var(--color-red-950); + + /* Orange */ + --orange-050: var(--color-orange-50); + --orange-100: var(--color-orange-100); + --orange-200: var(--color-orange-200); + --orange-300: var(--color-orange-300); + --orange-400: var(--color-orange-400); + --orange-500: var(--color-orange-500); + --orange-600: var(--color-orange-600); + --orange-700: var(--color-orange-700); + --orange-800: var(--color-orange-800); + --orange-900: var(--color-orange-900); + --orange-950: var(--color-orange-950); + + /* Amber */ + --amber-050: var(--color-amber-50); + --amber-100: var(--color-amber-100); + --amber-200: var(--color-amber-200); + --amber-300: var(--color-amber-300); + --amber-400: var(--color-amber-400); + --amber-500: var(--color-amber-500); + --amber-600: var(--color-amber-600); + --amber-700: var(--color-amber-700); + --amber-800: var(--color-amber-800); + --amber-900: var(--color-amber-900); + --amber-950: var(--color-amber-950); + + /* Yellow */ + --yellow-050: var(--color-yellow-50); + --yellow-100: var(--color-yellow-100); + --yellow-200: var(--color-yellow-200); + --yellow-300: var(--color-yellow-300); + --yellow-400: var(--color-yellow-400); + --yellow-500: var(--color-yellow-500); + --yellow-600: var(--color-yellow-600); + --yellow-700: var(--color-yellow-700); + --yellow-750: var(--color-yellow-750); + --yellow-800: var(--color-yellow-800); + --yellow-900: var(--color-yellow-900); + --yellow-950: var(--color-yellow-950); + + /* Lime */ + --lime-050: var(--color-lime-50); + --lime-100: var(--color-lime-100); + --lime-200: var(--color-lime-200); + --lime-300: var(--color-lime-300); + --lime-400: var(--color-lime-400); + --lime-500: var(--color-lime-500); + --lime-600: var(--color-lime-600); + --lime-700: var(--color-lime-700); + --lime-800: var(--color-lime-800); + --lime-900: var(--color-lime-900); + --lime-950: var(--color-lime-950); + + /* Green */ + --green-050: var(--color-green-50); + --green-100: var(--color-green-100); + --green-200: var(--color-green-200); + --green-300: var(--color-green-300); + --green-400: var(--color-green-400); + --green-500: var(--color-green-500); + --green-600: var(--color-green-600); + --green-700: var(--color-green-700); + --green-800: var(--color-green-800); + --green-900: var(--color-green-900); + --green-950: var(--color-green-950); + + /* Emerald */ + --emerald-050: var(--color-emerald-50); + --emerald-100: var(--color-emerald-100); + --emerald-200: var(--color-emerald-200); + --emerald-300: var(--color-emerald-300); + --emerald-400: var(--color-emerald-400); + --emerald-500: var(--color-emerald-500); + --emerald-550: var(--color-emerald-550); + --emerald-600: var(--color-emerald-600); + --emerald-700: var(--color-emerald-700); + --emerald-800: var(--color-emerald-800); + --emerald-900: var(--color-emerald-900); + --emerald-950: var(--color-emerald-950); + + /* Teal */ + --teal-050: var(--color-teal-50); + --teal-100: var(--color-teal-100); + --teal-200: var(--color-teal-200); + --teal-300: var(--color-teal-300); + --teal-400: var(--color-teal-400); + --teal-500: var(--color-teal-500); + --teal-550: var(--color-teal-550); + --teal-600: var(--color-teal-600); + --teal-700: var(--color-teal-700); + --teal-800: var(--color-teal-800); + --teal-900: var(--color-teal-900); + --teal-950: var(--color-teal-950); + + /* Cyan */ + --cyan-050: var(--color-cyan-50); + --cyan-100: var(--color-cyan-100); + --cyan-200: var(--color-cyan-200); + --cyan-300: var(--color-cyan-300); + --cyan-400: var(--color-cyan-400); + --cyan-500: var(--color-cyan-500); + --cyan-600: var(--color-cyan-600); + --cyan-700: var(--color-cyan-700); + --cyan-800: var(--color-cyan-800); + --cyan-900: var(--color-cyan-900); + --cyan-950: var(--color-cyan-950); + + /* Sky */ + --sky-050: var(--color-sky-50); + --sky-100: var(--color-sky-100); + --sky-200: var(--color-sky-200); + --sky-300: var(--color-sky-300); + --sky-400: var(--color-sky-400); + --sky-500: var(--color-sky-500); + --sky-600: var(--color-sky-600); + --sky-700: var(--color-sky-700); + --sky-800: var(--color-sky-800); + --sky-900: var(--color-sky-900); + --sky-950: var(--color-sky-950); + + /* Blue */ + --blue-050: var(--color-blue-50); + --blue-100: var(--color-blue-100); + --blue-200: var(--color-blue-200); + --blue-300: var(--color-blue-300); + --blue-400: var(--color-blue-400); + --blue-500: var(--color-blue-500); + --blue-600: var(--color-blue-600); + --blue-700: var(--color-blue-700); + --blue-800: var(--color-blue-800); + --blue-900: var(--color-blue-900); + --blue-950: var(--color-blue-950); + + /* Indigo */ + --indigo-050: var(--color-indigo-50); + --indigo-100: var(--color-indigo-100); + --indigo-200: var(--color-indigo-200); + --indigo-300: var(--color-indigo-300); + --indigo-400: var(--color-indigo-400); + --indigo-500: var(--color-indigo-500); + --indigo-600: var(--color-indigo-600); + --indigo-700: var(--color-indigo-700); + --indigo-800: var(--color-indigo-800); + --indigo-900: var(--color-indigo-900); + --indigo-950: var(--color-indigo-950); + + /* Violet */ + --violet-050: var(--color-violet-50); + --violet-100: var(--color-violet-100); + --violet-200: var(--color-violet-200); + --violet-300: var(--color-violet-300); + --violet-400: var(--color-violet-400); + --violet-500: var(--color-violet-500); + --violet-600: var(--color-violet-600); + --violet-700: var(--color-violet-700); + --violet-800: var(--color-violet-800); + --violet-900: var(--color-violet-900); + --violet-950: var(--color-violet-950); + + /* Purple */ + --purple-050: var(--color-purple-50); + --purple-100: var(--color-purple-100); + --purple-200: var(--color-purple-200); + --purple-300: var(--color-purple-300); + --purple-400: var(--color-purple-400); + --purple-500: var(--color-purple-500); + --purple-600: var(--color-purple-600); + --purple-700: var(--color-purple-700); + --purple-800: var(--color-purple-800); + --purple-900: var(--color-purple-900); + --purple-950: var(--color-purple-950); + + /* Fuchsia */ + --fuchsia-050: var(--color-fuchsia-50); + --fuchsia-100: var(--color-fuchsia-100); + --fuchsia-200: var(--color-fuchsia-200); + --fuchsia-300: var(--color-fuchsia-300); + --fuchsia-400: var(--color-fuchsia-400); + --fuchsia-500: var(--color-fuchsia-500); + --fuchsia-600: var(--color-fuchsia-600); + --fuchsia-700: var(--color-fuchsia-700); + --fuchsia-800: var(--color-fuchsia-800); + --fuchsia-900: var(--color-fuchsia-900); + --fuchsia-950: var(--color-fuchsia-950); + + /* Pink */ + --pink-050: var(--color-pink-50); + --pink-100: var(--color-pink-100); + --pink-200: var(--color-pink-200); + --pink-300: var(--color-pink-300); + --pink-400: var(--color-pink-400); + --pink-500: var(--color-pink-500); + --pink-600: var(--color-pink-600); + --pink-700: var(--color-pink-700); + --pink-800: var(--color-pink-800); + --pink-900: var(--color-pink-900); + --pink-950: var(--color-pink-950); + + /* Rose */ + --rose-050: var(--color-rose-50); + --rose-100: var(--color-rose-100); + --rose-200: var(--color-rose-200); + --rose-300: var(--color-rose-300); + --rose-400: var(--color-rose-400); + --rose-500: var(--color-rose-500); + --rose-600: var(--color-rose-600); + --rose-700: var(--color-rose-700); + --rose-800: var(--color-rose-800); + --rose-900: var(--color-rose-900); + --rose-950: var(--color-rose-950); + + /** + * Spacing + */ + --2xs: var(--c-spacing-xs); /* 2px -> closest is xs (2px) */ + --xs: var(--c-spacing-sm); /* 4px -> sm (4px) */ + --s: var(--c-spacing-md); /* 8px -> md (8px) */ + --m: var(--c-spacing-lg); /* 14px -> closest is lg (16px) */ + --l: var(--c-spacing-lg); /* 18px -> lg (16px) */ + --xl: var(--c-spacing-xl); /* 24px -> xl (32px) - note: different values */ + --padding: var(--c-spacing-lg); + --neg-padding: calc(var(--c-spacing-lg) * -1); + + /** + * Sizing + */ + --size-touch-target: calc(24rem / 16); + --size-icon: var(--c-size-icon-md); + --size-line-height: var(--c-leading-normal); + --touch-target-size: var(--size-touch-target); + --icon-size: var(--c-size-icon-md); + --lh: var(--c-leading-normal); + + /** + * Border Radius + */ + --radius-sm: var(--c-radius-sm); + --radius-md: var(--c-radius-md); + --radius-lg: var(--c-radius-lg); + --radius-full: var(--c-radius-full); + --radius-circle: 50%; + --border-radius: var(--c-radius-md); + --small-border-radius: var(--c-radius-sm); + --medium-border-radius: var(--c-radius-md); + --large-border-radius: var(--c-radius-lg); + --menu-border-radius: var(--c-radius-md); + + /** + * Typography + */ + --font-size: var(--c-text-base); + --font-weight-regular: 400; + --font-weight-bold: 700; + + /** + * Surfaces / Backgrounds + */ + --body-bg: var(--c-surface-default); + --bg-color: var(--c-surface-default); + --modal-bg: var(--c-surface-overlay); + --login-modal-bg: var(--c-surface-overlay); + --header-bg: var(--c-surface-raised); + --sidebar-bg: var(--c-surface-default); + --secondary-pane-bg: var(--c-surface-raised); + --shade-bg: rgba(0 0 0 / 25%); + --input-bg: var(--c-input-fill); + + /** + * Semantic Background Colors + */ + --bg-primary: var(--c-button-primary-fill); + --bg-secondary: var(--c-color-neutral-fill-loud); + --bg-selection-light: var(--c-color-accent-fill-quiet); + --bg-selection-dark: var(--c-color-accent-fill-normal); + --bg-error: var(--c-color-danger-fill-quiet); + --bg-warning: var(--c-color-warning-fill-quiet); + --bg-success: var(--c-color-success-fill-quiet); + --bg-notice: var(--c-color-info-fill-quiet); + --bg-enabled: var(--c-status-enabled-fill); + --bg-pending: var(--c-status-pending-fill); + --bg-disabled: var(--c-status-disabled-fill); + + /** + * Text Colors + */ + --text-color: var(--c-text-default); + --light-text-color: var(--c-text-quiet); + --medium-text-color: var(--c-text-quiet); + --medium-dark-text-color: var(--c-text-quiet); + --link-color: var(--c-text-link); + + /** + * Semantic Foreground Colors + */ + --fg-subtle: var(--c-text-quiet); + --fg-error: var(--c-color-danger-on-normal); + --fg-warning: var(--c-color-warning-on-normal); + --fg-success: var(--c-color-success-on-normal); + --fg-notice: var(--c-color-info-on-normal); + --fg-input: var(--c-text-default); + + /** + * Borders + */ + --border-hairline: var(--c-color-neutral-border-quiet); + --border-hairline-medium: var(--c-color-neutral-border-normal); + --border-hairline-dark: var(--c-color-neutral-border-loud); + --hairline-color: var(--c-color-neutral-border-quiet); + --medium-hairline-color: var(--c-color-neutral-border-normal); + --dark-hairline-color: var(--c-color-neutral-border-loud); + --pane-border: var(--c-color-neutral-border-quiet); + --header-border: var(--c-color-neutral-border-quiet); + --hr-border: var(--c-color-neutral-border-quiet); + --input-border: 1px solid var(--c-input-border-color); + --input-border-color: var(--c-input-border-color); + + /** + * Semantic Border Colors + */ + --border-primary: var(--c-color-brand-border-loud); + --border-secondary: var(--c-color-neutral-border-loud); + --border-error: var(--c-color-danger-border-quiet); + --border-warning: var(--c-color-warning-border-quiet); + --border-warning-emphasis: var(--c-color-warning-border-loud); + --border-success: var(--c-color-success-border-quiet); + --border-notice: var(--c-color-info-border-quiet); + + /** + * Focus States + */ + --focus-ring-color: var(--c-color-accent-border-normal); + --focus-outline-light: var(--c-color-accent-border-quiet); + --focus-outline-medium: var(--c-color-accent-border-normal); + --focus-outline-dark: var(--c-color-accent-border-loud); + --light-focus-color: var(--c-color-accent-border-quiet); + --medium-focus-color: var(--c-color-accent-border-normal); + --dark-focus-color: var(--c-color-accent-border-loud); + --focus-ring-alpha: 0.85; + --focus-ring: var(--CHANGE); /* Complex shadow value - needs manual review */ + --focus-ring-light: var(--CHANGE); + --focus-ring-medium: var(--CHANGE); + --focus-ring-dark: var(--CHANGE); + --focus-ring-outset: var(--CHANGE); + --focus-ring-inner: var(--CHANGE); + --focus-ring-inner-light: var(--CHANGE); + --light-focus-ring: var(--CHANGE); + --medium-focus-ring: var(--CHANGE); + --dark-focus-ring: var(--CHANGE); + --inner-focus-ring: var(--CHANGE); + + /** + * Shadows + */ + --modal-shadow: var(--c-modal-shadow); + --pane-shadow: var(--c-pane-shadow); + --secondary-pane-shadow: var(--c-shadow-sm); + + /** + * UI Controls + */ + --ui-control-fg: var(--c-text-quiet); + --ui-control-fg-hover: var(--c-text-default); + --ui-control-fg-active: var(--c-text-default); + --ui-control-bg: var(--c-form-control-fill); + --ui-control-bg-static: var(--c-form-control-fill); + --ui-control-bg-hover: var(--c-form-control-fill); + --ui-control-bg-active: var(--c-form-control-fill); + --ui-control-border: var(--c-form-control-border-color); + --ui-control-radius: var(--c-form-control-radius); + --ui-control-height: var(--c-form-control-height); + --ui-control-height-small: var(--c-size-control-sm); + --ui-control-height--small: var(--c-size-control-sm); + --ui-control-border-radius: var(--c-form-control-radius); + --ui-control-color: var(--c-text-quiet); + --ui-control-hover-color: var(--c-text-default); + --ui-control-active-color: var(--c-text-default); + --ui-control-bg-color: var(--c-form-control-fill); + --ui-control-hover-bg-color: var(--c-form-control-fill); + --ui-control-active-bg-color: var(--c-form-control-fill); + --ui-control-static-bg-color: var(--c-form-control-fill); + + /** + * Buttons + */ + --button-bg: var(--c-button-default-fill); + --button-bg--hover: var(--c-button-default-fill-hover); + --button-bg--active: var(--c-button-default-fill-hover); + --button-border: var(--c-button-default-border); + --button-border--hover: var(--c-button-default-border-hover); + --button-border--active: var(--c-button-default-border-hover); + --button-border-radius: var(--c-form-control-radius); + --button-padding: var(--c-form-control-spacing-inline); + --primary-button-bg: var(--c-button-primary-fill); + --primary-button-bg--hover: var(--c-button-primary-fill-hover); + --primary-button-bg--active: var(--c-button-primary-fill-hover); + --primary-button-border: var(--c-button-primary-border); + --primary-button-border--hover: var(--c-button-primary-border-hover); + --primary-button-border--active: var(--c-button-primary-border-hover); + --primary-button-text-color: var(--c-button-primary-text); + --secondary-button-bg: var(--c-button-default-fill); + --secondary-button-bg--hover: var(--c-button-default-fill-hover); + --secondary-button-bg--active: var(--c-button-default-fill-hover); + --secondary-button-border: var(--c-button-default-border); + --secondary-button-border--hover: var(--c-button-default-border-hover); + --secondary-button-border--active: var(--c-button-default-border-hover); + --secondary-button-text-color: var(--c-button-default-text); + + /** + * Inputs + */ + --input-height: var(--c-form-control-height); + --input-border-radius: var(--c-input-radius); + + /** + * Panes + */ + --pane-padding: var(--c-pane-padding); + --pane-border-radius: var(--c-pane-radius); + --pane-x-padding-default: var(--c-pane-padding); + --pane-y-padding-default: var(--c-pane-padding); + --secondary-pane-border: var(--c-color-neutral-border-quiet); + --secondary-pane-border-radius: var(--c-pane-radius); + + /** + * Modals + */ + --modal-padding: var(--c-modal-padding); + + /** + * Layout + */ + --header-height: var(--CHANGE); /* Layout-specific - needs manual value */ + --header-padding: var(--c-spacing-lg); + --content-padding: var(--c-pane-padding); + --global-sidebar-width: var( + --CHANGE + ); /* Layout-specific - needs manual value */ + + --hr-margin: var(--c-spacing-lg); + + /** + * Status Colors + */ + --enabled-color: var(--c-status-enabled-fill); + --pending-color: var(--c-status-pending-fill); + --disabled-color: var(--c-status-disabled-fill); + --status-label-bg-color: var(--CHANGE); + --status-label-text-color: var(--CHANGE); + + /** + * Nav Items + */ + --nav-item-indicator-size: var(--c-spacing-sm); + --nav-item-badge-bg: var(--c-color-neutral-fill-loud); + --nav-item-badge-fg: var(--c-text-white); + --nav-item-fg-active: var(--c-text-default); + --nav-item-fg-hover: var(--c-text-default); + --nav-item-bg-active: var(--c-color-neutral-fill-quiet); + --nav-item-bg-hover: var(--c-color-neutral-fill-quiet); + --nav-item-prefix-width: 1.875rem; + --nav-item-prefix-ratio: 1; + --nav-item-trigger-size: var(--c-size-touch-target); + --nav-item-gutter-width: var(--c-spacing-md); + --nav-item-badge-bgColor: var(--c-color-neutral-fill-loud); + --nav-item-badge-fgColor: var(--c-text-white); + --nav-item-fgColor-active: var(--c-text-default); + --nav-item-bgColor-active: var(--c-color-neutral-fill-quiet); + --nav-item-fgColor-hover: var(--c-text-default); + --nav-item-bgColor-hover: var(--c-color-neutral-fill-quiet); + --sidebar-bgColor: var(--c-surface-default); + + /** + * Miscellaneous + */ + --icon-color: var(--c-text-quiet); + --highlight-color: var(--c-color-accent-fill-quiet); + --selected-bg-color: var(--c-color-accent-fill-quiet); + --selected-item-color: var(--c-color-accent-fill-normal); + --hover-bg-color: var(--c-color-neutral-fill-quiet); + --interaction-background-color: var(--c-color-neutral-fill-quiet); + --light-sel-color: var(--c-color-accent-fill-quiet); + --dark-sel-color: var(--c-color-accent-fill-normal); + --primary-color: var(--c-button-primary-fill); + --secondary-color: var(--c-color-neutral-fill-loud); + --input-color: var(--c-text-default); + --error-color: var(--c-color-danger-on-normal); + --warning-color: var(--c-color-warning-on-normal); + --warning-background: var(--c-color-warning-fill-quiet); + --warning-border-color: var(--c-color-warning-border-quiet); + --success-color: var(--c-color-success-on-normal); + --notice-color: var(--c-color-info-on-normal); + --notice-background: var(--c-color-info-fill-quiet); + --notice-border-color: var(--c-color-info-border-quiet); + --indicator-border-color: var(--c-color-warning-border-loud); + --indicator-icon-color: var(--c-color-warning-on-normal); + --tab-label-color: var(--c-text-quiet); + --overlay-opacity: 0.5; + --checkbox-size: var(--c-size-control-xs); + --radio-size: var(--c-size-control-xs); + --thumb-size: var(--c-size-control-sm); + + /** + * Slideout + */ + --slideout-footer-bg: var(--c-surface-raised); + --slideout-footer-border: var(--c-color-neutral-border-quiet); + --slideout-footer-shadow: var(--c-shadow-lg); + + /** + * Variables that need context-specific values + * These are used in specific components and may need manual mapping + */ + --custom-bg-color: var(--c-color-fill-quiet); + --custom-border-color: var(--c-color-border-quiet); + --custom-text-color: var(--c-color-on-quiet); + --custom-titlebar-bg-color: var(--CHANGE); + --custom-sel-titlebar-bg-color: var(--CHANGE); + --custom-sel-tab-shadow-color: var(--CHANGE); + --outline-color: var(--c-color-accent-border-normal); + + /** + * Animation/Transform variables (context-specific) + */ + --disclosure-icon-transform: var(--CHANGE); + --disclosure-icon-transform-active: var(--CHANGE); + --arrow-angle: var(--CHANGE); + --arrow-c: var(--CHANGE); + --arrow-height: var(--CHANGE); + --arrow-width: var(--CHANGE); + --background-position-x: var(--CHANGE); + --background-position-y: var(--CHANGE); + + /** + * Layout-specific variables (need actual values) + */ + --details-width: var(--CHANGE); + --heading-width: var(--CHANGE); + --page-title-columns: var(--CHANGE); + --row-gap: var(--c-spacing-md); + --separator-width: 1px; + --size: var(--CHANGE); + --size-main-content: var(--CHANGE); + --source-item-toggle-size: var(--c-size-touch-target); + --spacing: var(--c-spacing); + --width: var(--CHANGE); + --max-lines: var(--CHANGE); + + /** + * Lightswitch specific + */ + --_lightswitch-border-color: var(--c-form-control-border-color); +} diff --git a/packages/craftcms-legacy/cp/src/css/_hud.scss b/packages/craftcms-legacy/cp/src/css/_hud.scss new file mode 100644 index 00000000000..bc2bf7b326c --- /dev/null +++ b/packages/craftcms-legacy/cp/src/css/_hud.scss @@ -0,0 +1,291 @@ +@use 'sass:color'; +@use 'variables'; +@use '@craftcms/sass/mixins'; + +.modal, +.hud { + z-index: 100; + box-sizing: border-box; +} + +.modal, +.hud { + @include mixins.modal; +} + +.header, +.hud-header, +.footer, +.hud-footer { + position: relative; + z-index: 1; + box-sizing: border-box; +} + +.header, +.hud-header, +.footer, +.hud-footer { + background-color: var(--gray-100); +} + +.header, +.hud-header { + border-radius: var(--radius-lg) var(--radius-lg) 0 0; + padding: 24px; + box-shadow: inset 0 -1px 0 var(--border-hairline); + + h1 { + margin: 0; + } + + &::after { + @include mixins.clearafter; + } +} + +.footer, +.hud-footer { + border-radius: 0 0 var(--radius-lg) var(--radius-lg); + padding-block: 5px; + padding-inline: var(--pane-padding, var(--xl)); + box-shadow: inset 0 1px 0 var(--border-hairline); + + &.flex { + & > * { + margin-block-end: 0; + } + } +} + +.modal .body, +.hud .main { + padding: var(--modal-padding); + overflow: hidden; + box-sizing: border-box; +} + +.pane, +.modal .body { + .header { + margin-block: calc(var(--pane-padding, 24px) * -1) var(--pane-padding, 24px); + margin-inline: calc(var(--pane-padding, 24px) * -1); + } + + .footer { + margin-block: var(--pane-padding, 24px) calc(var(--pane-padding, 24px) * -1); + margin-inline: calc(var(--pane-padding, 24px) * -1); + } +} + +.modal-shade, +.hud-shade { + z-index: 100; + position: fixed; + inset-block-start: 0; + inset-inline-start: 0; + width: 100%; + height: 100%; + + &:not(.visible) { + display: none; + } +} + +.modal-shade { + background: var(--shade-bg); + + &.blurred { + backdrop-filter: blur(10px); + } +} + +.modal { + position: fixed; + overflow: hidden; + + &:not(.fitted, .fullscreen) { + width: 66%; + height: 66%; + min-width: 600px; + min-height: 400px; + } + + &.fitted { + width: auto; + height: auto; + min-width: 0; + min-height: 0; + } + + &.fullscreen { + width: 100%; + height: 100%; + border-radius: 0; + } + + &.alert .body { + padding-inline-start: 76px; + + &::before { + @include mixins.icon; + margin-inline: -58px 0; + margin-block: -6px 0; + float: inline-start; + content: 'alert'; + font-size: 40px; + color: var(--light-text-color); + } + } + + &.secure .body { + padding-inline-start: 76px; + + &::before { + @include mixins.icon; + margin-inline: -56px 0; + margin-block: -14px 0; + float: inline-start; + content: 'secure'; + font-size: 58px; + color: var(--light-text-color); + } + } + + .resizehandle { + position: absolute; + z-index: 1; + inset-block-end: 0; + inset-inline-end: 0; + width: 24px; + height: 24px; + cursor: nwse-resize; + padding: var(--xs); + + path { + fill: var(--ui-control-color); + } + + body.rtl & { + .ltr { + display: none; + } + } + + body.ltr & { + .rtl { + display: none; + } + } + } +} + +.hud { + position: absolute; + display: none; + inset-block-start: 0; + + &.tooltip-hud { + display: block; + } + + &.has-footer .tip-bottom { + background-image: url('../images/hudtip_bottom_gray.png'); + } + + .tip { + position: absolute; + z-index: 101; + background: no-repeat 0 0; + } + + .tip-left { + inset-inline-start: -15px; + width: 15px; + height: 30px; + background-image: url('../images/hudtip_left.png'); + } + + .tip-top { + inset-block-start: -15px; + width: 30px; + height: 15px; + background-image: url('../images/hudtip_top.png'); + } + + .tip-right { + inset-inline-end: -15px; + width: 15px; + height: 30px; + background-image: url('../images/hudtip_right.png'); + } + + .tip-bottom { + inset-block-end: -15px; + width: 30px; + height: 15px; + background-image: url('../images/hudtip_bottom.png'); + } +} + +.hud .hud-header, +.hud .hud-footer { + padding-block: var(--s); + padding-inline: var(--xl); +} + +.hud .body { + overflow: hidden; + + ::-webkit-scrollbar { + appearance: none; + } + + ::-webkit-scrollbar:vertical { + width: 11px; + } + + ::-webkit-scrollbar:horizontal { + height: 11px; + } + + ::-webkit-scrollbar-thumb { + border-radius: 8px; + border: 2px solid transparent; + background-color: color.adjust(mixins.$black, $alpha: -0.5); + } + + ::-webkit-scrollbar-track { + background-color: var(--gray-050); + } +} + +/* ---------------------------------------- +/* Retina graphics +/* ---------------------------------------- */ + +@media only screen and (resolution >= 1.5dppx) { + .hud .tip-left { + background-image: url('../images/hudtip_left_2x.png'); + background-size: 15px 30px; + } + + .hud .tip-top { + background-image: url('../images/hudtip_top_2x.png'); + background-size: 30px 15px; + } + + .hud .tip-right { + background-image: url('../images/hudtip_right_2x.png'); + background-size: 15px 30px; + } + + .hud .tip-bottom { + background-image: url('../images/hudtip_bottom_2x.png'); + background-size: 30px 15px; + } + + .hud.has-footer .tip-bottom { + background-image: url('../images/hudtip_bottom_gray_2x.png'); + } +} diff --git a/packages/craftcms-legacy/cp/src/css/_icon-picker.scss b/packages/craftcms-legacy/cp/src/css/_icon-picker.scss new file mode 100644 index 00000000000..c44e4f6ca69 --- /dev/null +++ b/packages/craftcms-legacy/cp/src/css/_icon-picker.scss @@ -0,0 +1,97 @@ +@use '@craftcms/sass/mixins'; + +/* icon picker */ +.icon-picker { + display: flex; + flex-direction: row; + align-items: center; + gap: var(--xs); +} + +.icon-picker--icon { + display: flex; + align-items: center; + justify-content: center; + @include mixins.input-styles; + border-radius: var(--ui-control-border-radius); + width: var(--ui-control-height); + height: var(--ui-control-height); + background: var(--gray-050); + + svg { + width: calc(20rem / 16); + height: calc(20rem / 16); + @include mixins.svg-mask(var(--ui-control-color)); + } + + &.small { + width: calc(22rem / 16); + height: calc(22rem / 16); + + svg { + width: calc(14rem / 16); + height: calc(14rem / 16); + } + } +} + +.icon-picker-modal { + --width: calc(var(--ui-control-height) * 10 + var(--s) * 9 + var(--xl) * 2); + width: var(--width) !important; + min-width: 0 !important; + max-width: calc(100% - 20px) !important; + + .body { + height: 100%; + display: flex; + flex-direction: column; + gap: var(--l); + + .icon-picker-modal--list { + flex: 1; + position: relative; + overflow: hidden; + + &:not(.loading) { + .spinner { + display: none; + } + } + + &.loading { + &::after { + position: absolute; + inset-block-start: 0; + inset-inline-start: 0; + width: 100%; + height: 100%; + content: ''; + background-color: rgb(255 255 255 / 75%); + } + + .spinner { + inset-block-start: calc(50% - 10px); + z-index: 1; + } + } + + ul { + display: flex; + flex-flow: row wrap; + justify-content: flex-start; + gap: var(--s); + max-height: 100%; + overflow: auto; + + .icon-picker--icon { + --focus-ring: var(--focus-ring-inner); + + &:hover { + border-color: var(--link-color); + background-color: var(--blue-100); + } + } + } + } + } +} diff --git a/packages/craftcms-legacy/cp/src/css/_login.scss b/packages/craftcms-legacy/cp/src/css/_login.scss index 4b686e558f4..1c49ad24a4a 100644 --- a/packages/craftcms-legacy/cp/src/css/_login.scss +++ b/packages/craftcms-legacy/cp/src/css/_login.scss @@ -29,6 +29,7 @@ body.login { } main { + min-height: 100vh; flex: 1; display: flex; flex-direction: column; diff --git a/packages/craftcms-legacy/cp/src/css/_main.scss b/packages/craftcms-legacy/cp/src/css/_main.scss index 71c4ead1a24..87c6d13fd8e 100644 --- a/packages/craftcms-legacy/cp/src/css/_main.scss +++ b/packages/craftcms-legacy/cp/src/css/_main.scss @@ -734,16 +734,6 @@ h2[data-icon]::before { font-size: 19px; } -/* horizontal rule */ -hr { - margin-block: var(--hr-margin); - margin-inline: 0; - border: none; - border-block-start: var(--hr-border); - height: 0; - color: transparent; -} - /* paragraphs */ p { margin-block: 1em; @@ -1830,47 +1820,6 @@ ul.icons { } } -.flex { - display: flex; - align-items: center; - align-content: stretch; - gap: var(--s); - - &.flex-gap-xs { - gap: var(--xs); - } - - &.flex-gap-m { - gap: var(--m); - } - - &.flex-gap-l { - gap: var(--l); - } - - &.flex-gap-xl { - gap: var(--xl); - } - - &:not(.flex-nowrap) { - flex-wrap: wrap; - } - - & > * { - &.label { - white-space: nowrap; - } - } - - .centeralign & { - justify-content: center; - } - - &.rightalign { - justify-content: right; - } -} - .inline-flex { display: inline-flex !important; align-items: center; @@ -3158,7 +3107,7 @@ h2 + .actions { } } -table { +table:not(.cp-table) { &.fixed-layout { table-layout: fixed; } @@ -3551,7 +3500,7 @@ table { .chip, .card { - overflow: hidden; // needed for status indicators + overflow: clip; // needed for status indicators &.sel, tr.sel &, @@ -6501,743 +6450,201 @@ $min2ColWidth: 400px; position: relative; } -.slideout-container, -.slideout, -.modal, -.hud { - z-index: 100; - box-sizing: border-box; -} - -.modal, -.hud { - @include mixins.modal; -} - -.slideout-shade { - opacity: 0; - transition: opacity linear 250ms; - - &.so-visible { - opacity: 1; - } -} +/* inline asset previews */ -.slideout-container { - position: fixed; - inset-block-start: 0; - inset-inline-start: 0; - width: 100vw; - height: 100vh; - height: -webkit-fill-available; // h/t https://twitter.com/AllThingsSmitty/status/1254151507412496384 - pointer-events: none; +.preview-thumb-container { + position: relative; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + height: 190px; + background-color: var(--gray-900); + margin-block: 0 var(--spacing); + margin-inline: var(--neg-padding); - &.so-lp { - position: fixed; - inset-block-start: var(--m); - inset-inline-start: var(--m); - width: calc(100% - var(--m) * 2); - height: calc(100vh - var(--m) * 2); + &.checkered img { + background-color: var(--white); + @include mixins.checkered-bg(17px); } - body.has-debug-toolbar & { - height: calc(100vh - 42px); + &.editable { + cursor: pointer; } -} - -.slideout { - position: absolute; - background-color: var(--modal-bg); - box-shadow: var(--modal-shadow) !important; - display: flex; - flex-direction: column; - overflow: hidden; - padding-block: 24px; - padding-inline: var(--padding); - pointer-events: all; - container-type: inline-size; - &.so-mobile, - &.so-lp { - width: 100% !important; - height: 100% !important; - inset-inline-start: 0; - transition: inset-block-start linear 250ms; - will-change: inset-block-start; - box-shadow: none !important; - } + &.loading { + &::after { + content: ''; + font-size: 0; + display: block; + position: absolute; + width: 100%; + height: 100%; + inset-inline-start: 0; + inset-block-start: 0; + background-color: color.adjust(mixins.$grey900, $alpha: -0.2); + } - &.so-mobile { - --padding: var(--m); - --neg-padding: calc(var(--m) * -1); + .spinner { + color: var(--white); + z-index: 1; + } } - &.so-lp { + #details & { border-radius: var(--radius-lg); + overflow: hidden; } - &:not(.so-mobile, .so-lp) { - border-start-start-radius: var(--radius-lg); - border-start-end-radius: 0; - border-end-end-radius: 0; - border-end-start-radius: var(--radius-lg); - } - - &:not(.so-mobile, .so-lp) { - inset-block-start: 0; - width: 55%; + .preview-thumb { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; height: 100%; - @media screen and (prefers-reduced-motion: no-preference) { - transition: - inset-inline-start linear 250ms, - inset-inline-end linear 250ms; - will-change: transform; + img { + display: block; + max-width: 100%; + max-height: 190px; } } +} - & > .pane-header { - padding-inline: var(--padding); - z-index: 2; - border-radius: 0; +.image-actions { + &.is-mobile { + margin-block: calc((var(--spacing) / 2) * -1) var(--spacing); + margin-inline: var(--neg-padding); + display: grid; + grid-template-columns: 1fr 1fr; + } +} - // keep the margin-block-start as it was for wider viewports - @media only screen and (width <= 767px) { - margin-block: calc(var(--xl) * -1) var(--padding); - } +.button-fade { + .buttons { + opacity: 0; + position: absolute; + inset-block-start: 10px; + inset-inline-end: 10px; + margin: 0; + transition: opacity linear 100ms; - & > .so-toolbar { - display: flex; - flex-direction: row; - align-items: center; - gap: var(--xs); - min-height: calc(44px - 16px); + .btn { + --ui-control-color: var(--white); + --ui-control-hover-color: var(--white); + --ui-control-active-color: var(--white); + --interaction-background-color: var(--gray-700); + background-color: var(--gray-600); + color: var(--white); + @include mixins.light-on-dark-text; + @include mixins.two-color-focus-ring($light-button: false); - & > .pane-tabs { - width: 1px; // give other elements in the header plenty of room before the tabs take up whatever's left - flex: 1; - margin-inline-end: 0; + &:hover { + background-color: var(--interaction-background-color) !important; + } + + &:not(.disabled, .loading, .dashed) { + &:focus, + &.focus, + &:hover { + background-color: var(--interaction-background-color); + } } } } - & > .so-body { - flex: 1; - margin-block: -24px; - margin-inline: var(--neg-padding); - overflow: hidden auto; - position: relative; - - &:not(:last-child) { - margin-block-end: 0; + &:hover, + &:focus-within { + .buttons { + opacity: 1; } + } +} - & > h1:not(:last-child) { - padding-block-end: var(--s); - border-block-end: 1px solid var(--border-hairline); - } +/* element selector modals */ +.elementselectormodal { + --content-padding: 24px; + padding-block-end: 44px; + user-select: none; + + & > .header { + padding-block: 14px; + text-align: center; - &.so-full-details, - & > .so-sidebar { - background-color: var(--gray-100) !important; + & + .body { + height: calc(100% - 48px) !important; } + } - &:not(.so-full-details) { - padding-block: 24px; - padding-inline: var(--padding); + .body { + position: relative; + height: 100%; + + .spinner.big { + position: absolute; + inset-block-start: 50%; + inset-inline-start: 50%; + margin-block: -24px 0; + margin-inline: -24px 0; } - & > .so-sidebar, - &.so-full-details > .so-content > .details { - box-sizing: border-box; - padding-block: 0 var(--spacing); - padding-inline: var(--padding); + .content { + height: calc(100% + 48px); - // set the margin-block-start so the slideout doesn't overlap the toolbar - @media only screen and (width <= 767px) { - margin-block-start: 16px; + .sidebar { + position: absolute; + inset-block-start: 0; + margin-inline-start: calc(-249rem / 16); + height: 100%; + overflow: auto; + padding-block: var(--content-padding); + padding-inline: 0; } - & > .preview-thumb-container { - margin-block: 0; - margin-inline: var(--neg-padding); - height: auto; - min-height: 54px; // make room for the Preview / Edit buttons + .main { + margin: -24px; + padding: var(--content-padding); + height: 100%; + box-sizing: border-box; + overflow: auto; + position: relative; - & + .pane-header { - border-radius: 0; - } - } + .elements { + &.busy { + min-height: calc(100% - 48px); - .image-actions { - &.is-mobile { - margin-block: calc(var(--spacing) / 2) var(--spacing); - margin-inline: 0; - } - } + .update-spinner { + z-index: 101; + } + } - & > .meta.read-only:first-child { - margin-block-start: var(--padding); - } + .header { + margin-block: 0 var(--m); + margin-inline: 0; + } - & > .meta.warning { - box-shadow: none; - border-block-end: 1px solid var(--yellow-300); - } + .tableview table { + tr { + th, + td { + cursor: default; + } - & > .field { - & > .input > .text.fullwidth { - border-radius: 0; - } - } + // prevent double focus ring (the row already gets it) + .checkbox::before { + box-shadow: none !important; + } + } + } - .notes { - padding-block: var(--m); + .structure .row { + margin-block-start: 1px; + } + } } } - - & > .so-sidebar { - position: absolute; - inset-block-start: 0; - @include mixins.pane; - width: 350px; - height: 100%; - max-width: 100%; - overflow: hidden auto; - z-index: 1001; - - body.ltr & { - transition: inset-inline-end linear 250ms; - } - - body.rtl & { - transition: inset-inline-start linear 250ms; - } - - & > fieldset:first-child { - margin-block-start: 24px !important; - } - } - } - - & > .so-footer { - position: relative; - display: flex; - gap: var(--s); - justify-content: flex-end; - flex-wrap: wrap; - margin-block: 0 -24px; - margin-inline: var(--neg-padding); - padding-block: 5px; - padding-inline: var(--padding); - border-block-start: var(--slideout-footer-border); - background: var(--slideout-footer-bg); - box-shadow: var(--slideout-footer-shadow); - z-index: 3; - - & > .so-notice { - display: flex; - align-items: center; - margin-inline-end: auto; - } - - & > .so-extra { - flex: 0 0 100%; - margin-block: 0; - margin-inline: var(--neg-padding); - padding-block: 0 8px; - padding-inline: var(--padding); - border-block-end: 1px solid var(--border-hairline); - } - } -} - -@container (width > 700px) { - .slideout { - &.showing-sidebar { - .so-body { - display: flex; - flex-direction: row; - padding: 0; - overflow: hidden; - - & > .so-content { - position: relative; - z-index: 2; - padding: 24px; - width: calc(100% - 350px); - height: 100%; - box-sizing: border-box; - border-inline-end: 1px solid var(--gray-200); - overflow: hidden auto; - } - - & > .so-sidebar { - position: relative; - display: block !important; - inset-block-start: auto; - inset-inline: auto !important; - height: 100%; - box-shadow: none; - } - } - } - - & > .so-footer { - & > .so-extra { - margin: 0; - padding: 0; - border: none; - flex: auto 0 1; - } - } - } -} - -.header, -.hud-header, -.footer, -.hud-footer { - position: relative; - z-index: 1; - box-sizing: border-box; -} - -.header, -.hud-header, -.footer, -.hud-footer { - background-color: var(--gray-100); -} - -.header, -.hud-header { - border-radius: var(--radius-lg) var(--radius-lg) 0 0; - padding: 24px; - box-shadow: inset 0 -1px 0 var(--border-hairline); - - h1 { - margin: 0; - } - - &::after { - @include mixins.clearafter; - } -} - -.footer, -.hud-footer { - border-radius: 0 0 var(--radius-lg) var(--radius-lg); - padding-block: 5px; - padding-inline: var(--pane-padding, var(--xl)); - box-shadow: inset 0 1px 0 var(--border-hairline); - - &.flex { - & > * { - margin-block-end: 0; - } - } -} - -.modal .body, -.hud .main { - padding: var(--modal-padding); - overflow: hidden; - box-sizing: border-box; -} - -.pane, -.modal .body { - .header { - margin-block: calc(var(--pane-padding, 24px) * -1) var(--pane-padding, 24px); - margin-inline: calc(var(--pane-padding, 24px) * -1); - } - - .footer { - margin-block: var(--pane-padding, 24px) calc(var(--pane-padding, 24px) * -1); - margin-inline: calc(var(--pane-padding, 24px) * -1); - } -} - -.slideout-shade, -.modal-shade, -.hud-shade { - z-index: 100; - position: fixed; - inset-block-start: 0; - inset-inline-start: 0; - width: 100%; - height: 100%; - - &:not(.visible) { - display: none; - } -} - -.slideout-shade, -.modal-shade { - background: var(--shade-bg); - - &.blurred { - backdrop-filter: blur(10px); - } -} - -.modal { - position: fixed; - overflow: hidden; - - &:not(.fitted, .fullscreen) { - width: 66%; - height: 66%; - min-width: 600px; - min-height: 400px; - } - - &.fitted { - width: auto; - height: auto; - min-width: 0; - min-height: 0; - } - - &.fullscreen { - width: 100%; - height: 100%; - border-radius: 0; - } - - &.alert .body { - padding-inline-start: 76px; - - &::before { - @include mixins.icon; - margin-inline: -58px 0; - margin-block: -6px 0; - float: inline-start; - content: 'alert'; - font-size: 40px; - color: var(--light-text-color); - } - } - - &.secure .body { - padding-inline-start: 76px; - - &::before { - @include mixins.icon; - margin-inline: -56px 0; - margin-block: -14px 0; - float: inline-start; - content: 'secure'; - font-size: 58px; - color: var(--light-text-color); - } - } - - .resizehandle { - position: absolute; - z-index: 1; - inset-block-end: 0; - inset-inline-end: 0; - width: 24px; - height: 24px; - cursor: nwse-resize; - padding: var(--xs); - - path { - fill: var(--ui-control-color); - } - - body.rtl & { - .ltr { - display: none; - } - } - - body.ltr & { - .rtl { - display: none; - } - } - } -} - -.hud { - position: absolute; - display: none; - inset-block-start: 0; - - &.tooltip-hud { - display: block; - } - - &.has-footer .tip-bottom { - background-image: url('../images/hudtip_bottom_gray.png'); - } - - .tip { - position: absolute; - z-index: 101; - background: no-repeat 0 0; - } - - .tip-left { - inset-inline-start: -15px; - width: 15px; - height: 30px; - background-image: url('../images/hudtip_left.png'); - } - - .tip-top { - inset-block-start: -15px; - width: 30px; - height: 15px; - background-image: url('../images/hudtip_top.png'); - } - - .tip-right { - inset-inline-end: -15px; - width: 15px; - height: 30px; - background-image: url('../images/hudtip_right.png'); - } - - .tip-bottom { - inset-block-end: -15px; - width: 30px; - height: 15px; - background-image: url('../images/hudtip_bottom.png'); - } -} - -.hud .hud-header, -.hud .hud-footer { - padding-block: var(--s); - padding-inline: var(--xl); -} - -.hud .body { - overflow: hidden; - - ::-webkit-scrollbar { - appearance: none; - } - - ::-webkit-scrollbar:vertical { - width: 11px; - } - - ::-webkit-scrollbar:horizontal { - height: 11px; - } - - ::-webkit-scrollbar-thumb { - border-radius: 8px; - border: 2px solid transparent; - background-color: color.adjust(mixins.$black, $alpha: -0.5); - } - - ::-webkit-scrollbar-track { - background-color: var(--gray-050); - } -} - -/* inline asset previews */ - -.preview-thumb-container { - position: relative; - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - height: 190px; - background-color: var(--gray-900); - margin-block: 0 var(--spacing); - margin-inline: var(--neg-padding); - - &.checkered img { - background-color: var(--white); - @include mixins.checkered-bg(17px); - } - - &.editable { - cursor: pointer; - } - - &.loading { - &::after { - content: ''; - font-size: 0; - display: block; - position: absolute; - width: 100%; - height: 100%; - inset-inline-start: 0; - inset-block-start: 0; - background-color: color.adjust(mixins.$grey900, $alpha: -0.2); - } - - .spinner { - color: var(--white); - z-index: 1; - } - } - - #details & { - border-radius: var(--radius-lg); - overflow: hidden; - } - - .preview-thumb { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; - - img { - display: block; - max-width: 100%; - max-height: 190px; - } - } -} - -.image-actions { - &.is-mobile { - margin-block: calc((var(--spacing) / 2) * -1) var(--spacing); - margin-inline: var(--neg-padding); - display: grid; - grid-template-columns: 1fr 1fr; - } -} - -.button-fade { - .buttons { - opacity: 0; - position: absolute; - inset-block-start: 10px; - inset-inline-end: 10px; - margin: 0; - transition: opacity linear 100ms; - - .btn { - --ui-control-color: var(--white); - --ui-control-hover-color: var(--white); - --ui-control-active-color: var(--white); - --interaction-background-color: var(--gray-700); - background-color: var(--gray-600); - color: var(--white); - @include mixins.light-on-dark-text; - @include mixins.two-color-focus-ring($light-button: false); - - &:hover { - background-color: var(--interaction-background-color) !important; - } - - &:not(.disabled, .loading, .dashed) { - &:focus, - &.focus, - &:hover { - background-color: var(--interaction-background-color); - } - } - } - } - - &:hover, - &:focus-within { - .buttons { - opacity: 1; - } - } -} - -/* element selector modals */ -.elementselectormodal { - --content-padding: 24px; - padding-block-end: 44px; - user-select: none; - - & > .header { - padding-block: 14px; - text-align: center; - - & + .body { - height: calc(100% - 48px) !important; - } - } - - .body { - position: relative; - height: 100%; - - .spinner.big { - position: absolute; - inset-block-start: 50%; - inset-inline-start: 50%; - margin-block: -24px 0; - margin-inline: -24px 0; - } - - .content { - height: calc(100% + 48px); - - .sidebar { - position: absolute; - inset-block-start: 0; - margin-inline-start: calc(-249rem / 16); - height: 100%; - overflow: auto; - padding-block: var(--content-padding); - padding-inline: 0; - } - - .main { - margin: -24px; - padding: var(--content-padding); - height: 100%; - box-sizing: border-box; - overflow: auto; - position: relative; - - .elements { - &.busy { - min-height: calc(100% - 48px); - - .update-spinner { - z-index: 101; - } - } - - .header { - margin-block: 0 var(--m); - margin-inline: 0; - } - - .tableview table { - tr { - th, - td { - cursor: default; - } - - // prevent double focus ring (the row already gets it) - .checkbox::before { - box-shadow: none !important; - } - } - } - - .structure .row { - margin-block-start: 1px; - } - } - } - } - } + } .footer { position: absolute; @@ -7627,290 +7034,6 @@ $min2ColWidth: 400px; padding: var(--xl); } -/* ---------------------------------------- -/* Menus -/* ---------------------------------------- */ - -.menu, -.ui-datepicker, -.ui-timepicker-list { - @include mixins.menu-styles; -} - -.ui-datepicker, -.ui-timepicker-list { - padding: 0; -} - -.menu { - position: absolute; - - &:not(.visible) { - display: none; - } - - &.padded { - padding-block: var(--s); - padding-inline: calc(var(--m) + var(--s)); - - hr { - margin-block: var(--s); - margin-inline: calc((var(--m) + var(--s)) * -1); - } - - ul { - li { - margin-block: 0; - margin-inline: calc(var(--m) * -1); - padding-block: 0; - padding-inline: var(--m); - - a { - border-radius: var(--radius-lg); - } - } - } - - .extralight { - margin-block-start: 2px; - } - } - - h6, - .h6 { - &:first-child { - margin-block-start: 14px !important; - } - } - - ul { - &.padded { - li { - a, - .menu-item, - .menu-option { - padding-inline-start: calc(var(--m) + 18rem / 16); - - &.sel { - &:not([data-icon])::before { - float: inline-start; - margin-inline: calc(-18rem / 16 - 4px) 0; - margin-block: 4px 0; - font-size: 14px; - width: 14px; - content: 'check' / ''; - color: currentcolor; - margin-block-start: 3px !important; - } - } - } - } - } - - li { - a, - .menu-item, - .menu-option { - margin-block: 0; - margin-inline: -14px; - padding-block: 10px; - padding-inline: 14px; - white-space: nowrap; - font-size: 14px; - appearance: none; - - &:not(:last-child) { - margin-inline-end: 0; - } - - &:not(.flex, .hidden) { - display: block; - width: calc(100% + 28px); - text-align: start; - } - - &.flex { - [data-icon] { - margin-block-start: -2px; - } - } - - &.sel { - cursor: default; - } - - .shortcut { - float: inline-end; - margin-inline-start: 14px; - padding-block: 0; - padding-inline: 4px; - border-radius: var(--radius-md); - box-shadow: - 0 0 0 1px color.adjust(mixins.$grey600, $alpha: -0.75), - 0 1px 3px -1px color.adjust(mixins.$grey600, $alpha: -0.5); - } - - .infoicon-container { - display: inline-flex; - align-items: flex-start; - margin-inline-start: var(--s); - font-size: 0.8em; - } - - .menu-item-description:empty { - display: none; - } - } - } - } - - & > .flex { - margin-block: 10px; - position: relative; - - &.padded { - margin-inline-start: -14px; - padding-inline-start: 24px; - - &.sel { - &::before { - position: absolute; - inset-block-start: 36px; - inset-inline-start: 7px; - content: 'check'; - font-size: 14px; - color: var(--light-text-color); - } - } - } - } - - hr { - margin-block: 5px; - margin-inline: -14px; - } - - .go::after { - color: inherit; - } - - &:not(.menu--disclosure) ul li a, - ul li .menu-item, - ul li .menu-option { - color: mixins.$menuOptionColor; - text-decoration: none; - cursor: default; - } -} - -.menu li { - & > a, - & > button, - & > .menu-item { - &[data-icon]::before, - [data-icon]::before, - span.icon:not([data-icon]) { - display: inline-block; - width: calc(14rem / 16); - height: calc(14rem / 16); - margin-inline: -1px 9px; - } - - &[data-icon]::before, - [data-icon]::before { - position: relative; - inset-block-start: -2px; - text-align: center; - font-size: 14px; - color: currentcolor; - } - - span.icon:not([data-icon]) svg { - display: block; - position: relative; - inset-block-start: 1px; - width: 100%; - height: 100%; - @include mixins.svg-mask(var(--icon-color, var(--ui-control-color))); - } - - &.error { - &[data-icon]::before, - [data-icon]::before { - color: var(--fg-error); - } - - span.icon:not([data-icon]) svg { - @include mixins.svg-mask(var(--fg-error)); - } - } - - .chip { - display: flex; - margin-block: -4px; - } - } -} - -/* prettier-ignore */ -.menu:not(.menu--disclosure) ul li a:not(.disabled):hover, -.menu:not(.menu--disclosure) ul li .menu-item:not(.sel, .disabled):hover, -.menu:not(.menu--disclosure) ul li .menu-option:not(.sel, .disabled):hover, -.menu:not(.menu--disclosure, :hover) ul li a:not(.disabled).hover, -.menu:not(.menu--disclosure, :hover) ul li .menu-item:not(.sel, .disabled).hover, -.menu:not(.menu--disclosure, :hover) ul li .menu-option:not(.sel, .disabled).hover { - @include mixins.menu-item-active-styles; - - // Pretty gnarly, but needs to override the default hover styles with this selector - .status:not(.on, .live, .active, .enabled, .all) { - &:not(.pending, .warning, .off, .suspended, .expired) { - &:not(.light, .gray, .red, .orange, .amber, .yellow) { - &:not(.lime, .green, .emerald, .teal, .cyan, .sky) { - &:not(.blue, .indigo, .violet, .purple, .fuchsia) { - &:not(.pink, .rose, .grey, .black, .disabled) { - &:not(.inactive) { - border-color: currentcolor; - } - } - } - } - } - } - } -} - -.menu { - hr.padded, - .h6.padded, - h6.padded { - margin-inline-start: 20px; - } -} - -.menu--disclosure { - & > .search-container { - margin-inline: calc(var(--m) * -1); - padding: var(--m); - border-block-end: 1px solid var(--border-hairline); - } - - ul li { - &.filtered { - display: none; - } - - & > a:not(.crumb-link), - & > .menu-item, - & > .menu-option { - --focus-ring: var(--focus-ring-inner); - - &:hover { - @include mixins.disclosure-link-hover-styles; - } - } - } -} - /* tag select fields */ .tagselect { .elements { @@ -9472,102 +8595,6 @@ body { } } -/* icon picker */ -.icon-picker { - display: flex; - flex-direction: row; - align-items: center; - gap: var(--xs); -} - -.icon-picker--icon { - display: flex; - align-items: center; - justify-content: center; - @include mixins.input-styles; - border-radius: var(--ui-control-border-radius); - width: var(--ui-control-height); - height: var(--ui-control-height); - background: var(--gray-050); - - svg { - width: calc(20rem / 16); - height: calc(20rem / 16); - @include mixins.svg-mask(var(--ui-control-color)); - } - - &.small { - width: calc(22rem / 16); - height: calc(22rem / 16); - - svg { - width: calc(14rem / 16); - height: calc(14rem / 16); - } - } -} - -.icon-picker-modal { - --width: calc(var(--ui-control-height) * 10 + var(--s) * 9 + var(--xl) * 2); - width: var(--width) !important; - min-width: 0 !important; - max-width: calc(100% - 20px) !important; - - .body { - height: 100%; - display: flex; - flex-direction: column; - gap: var(--l); - - .icon-picker-modal--list { - flex: 1; - position: relative; - overflow: hidden; - - &:not(.loading) { - .spinner { - display: none; - } - } - - &.loading { - &::after { - position: absolute; - inset-block-start: 0; - inset-inline-start: 0; - width: 100%; - height: 100%; - content: ''; - background-color: rgb(255 255 255 / 75%); - } - - .spinner { - inset-block-start: calc(50% - 10px); - z-index: 1; - } - } - - ul { - display: flex; - flex-flow: row wrap; - justify-content: flex-start; - gap: var(--s); - max-height: 100%; - overflow: auto; - - .icon-picker--icon { - --focus-ring: var(--focus-ring-inner); - - &:hover { - border-color: var(--link-color); - background-color: var(--blue-100); - } - } - } - } - } -} - /* errors */ ul.errors { margin-block-start: 5px; @@ -10072,28 +9099,4 @@ body.sitepicker { background-image: url('../images/branch_rtl_2x.png'); } } - - .hud .tip-left { - background-image: url('../images/hudtip_left_2x.png'); - background-size: 15px 30px; - } - - .hud .tip-top { - background-image: url('../images/hudtip_top_2x.png'); - background-size: 30px 15px; - } - - .hud .tip-right { - background-image: url('../images/hudtip_right_2x.png'); - background-size: 15px 30px; - } - - .hud .tip-bottom { - background-image: url('../images/hudtip_bottom_2x.png'); - background-size: 30px 15px; - } - - .hud.has-footer .tip-bottom { - background-image: url('../images/hudtip_bottom_gray_2x.png'); - } } diff --git a/packages/craftcms-legacy/cp/src/css/_menu.scss b/packages/craftcms-legacy/cp/src/css/_menu.scss new file mode 100644 index 00000000000..b07d75f9d8c --- /dev/null +++ b/packages/craftcms-legacy/cp/src/css/_menu.scss @@ -0,0 +1,286 @@ +@use 'sass:color'; +@use '@craftcms/sass/mixins'; + +/* ---------------------------------------- +/* Menus +/* ---------------------------------------- */ + +.menu, +.ui-datepicker, +.ui-timepicker-list { + @include mixins.menu-styles; +} + +.ui-datepicker, +.ui-timepicker-list { + padding: 0; +} + +.menu { + position: absolute; + + &:not(.visible) { + display: none; + } + + &.padded { + padding-block: var(--s); + padding-inline: calc(var(--m) + var(--s)); + + hr { + margin-block: var(--s); + margin-inline: calc((var(--m) + var(--s)) * -1); + } + + ul { + li { + margin-block: 0; + margin-inline: calc(var(--m) * -1); + padding-block: 0; + padding-inline: var(--m); + + a { + border-radius: var(--radius-lg); + } + } + } + + .extralight { + margin-block-start: 2px; + } + } + + h6, + .h6 { + &:first-child { + margin-block-start: 14px !important; + } + } + + ul { + &.padded { + li { + a, + .menu-item, + .menu-option { + padding-inline-start: calc(var(--m) + 18rem / 16); + + &.sel { + &:not([data-icon])::before { + float: inline-start; + margin-inline: calc(-18rem / 16 - 4px) 0; + margin-block: 4px 0; + font-size: 14px; + width: 14px; + content: 'check' / ''; + color: currentcolor; + margin-block-start: 3px !important; + } + } + } + } + } + + li { + a, + .menu-item, + .menu-option { + margin-block: 0; + margin-inline: -14px; + padding-block: 10px; + padding-inline: 14px; + white-space: nowrap; + font-size: 14px; + appearance: none; + + &:not(:last-child) { + margin-inline-end: 0; + } + + &:not(.flex, .hidden) { + display: block; + width: calc(100% + 28px); + text-align: start; + } + + &.flex { + [data-icon] { + margin-block-start: -2px; + } + } + + &.sel { + cursor: default; + } + + .shortcut { + float: inline-end; + margin-inline-start: 14px; + padding-block: 0; + padding-inline: 4px; + border-radius: var(--radius-md); + box-shadow: + 0 0 0 1px color.adjust(mixins.$grey600, $alpha: -0.75), + 0 1px 3px -1px color.adjust(mixins.$grey600, $alpha: -0.5); + } + + .infoicon-container { + display: inline-flex; + align-items: flex-start; + margin-inline-start: var(--s); + font-size: 0.8em; + } + + .menu-item-description:empty { + display: none; + } + } + } + } + + & > .flex { + margin-block: 10px; + position: relative; + + &.padded { + margin-inline-start: -14px; + padding-inline-start: 24px; + + &.sel { + &::before { + position: absolute; + inset-block-start: 36px; + inset-inline-start: 7px; + content: 'check'; + font-size: 14px; + color: var(--light-text-color); + } + } + } + } + + hr { + margin-block: 5px; + margin-inline: -14px; + } + + .go::after { + color: inherit; + } + + &:not(.menu--disclosure) ul li a, + ul li .menu-item, + ul li .menu-option { + color: mixins.$menuOptionColor; + text-decoration: none; + cursor: default; + } +} + +.menu li { + & > a, + & > button, + & > .menu-item { + &[data-icon]::before, + [data-icon]::before, + span.icon:not([data-icon]) { + display: inline-block; + width: calc(14rem / 16); + height: calc(14rem / 16); + margin-inline: -1px 9px; + } + + &[data-icon]::before, + [data-icon]::before { + position: relative; + inset-block-start: -2px; + text-align: center; + font-size: 14px; + color: currentcolor; + } + + span.icon:not([data-icon]) svg { + display: block; + position: relative; + inset-block-start: 1px; + width: 100%; + height: 100%; + @include mixins.svg-mask(var(--icon-color, var(--ui-control-color))); + } + + &.error { + &[data-icon]::before, + [data-icon]::before { + color: var(--fg-error); + } + + span.icon:not([data-icon]) svg { + @include mixins.svg-mask(var(--fg-error)); + } + } + + .chip { + display: flex; + margin-block: -4px; + } + } +} + +/* prettier-ignore */ +.menu:not(.menu--disclosure) ul li a:not(.disabled):hover, +.menu:not(.menu--disclosure) ul li .menu-item:not(.sel, .disabled):hover, +.menu:not(.menu--disclosure) ul li .menu-option:not(.sel, .disabled):hover, +.menu:not(.menu--disclosure, :hover) ul li a:not(.disabled).hover, +.menu:not(.menu--disclosure, :hover) ul li .menu-item:not(.sel, .disabled).hover, +.menu:not(.menu--disclosure, :hover) ul li .menu-option:not(.sel, .disabled).hover { + @include mixins.menu-item-active-styles; + + // Pretty gnarly, but needs to override the default hover styles with this selector + .status:not(.on, .live, .active, .enabled, .all) { + &:not(.pending, .warning, .off, .suspended, .expired) { + &:not(.light, .gray, .red, .orange, .amber, .yellow) { + &:not(.lime, .green, .emerald, .teal, .cyan, .sky) { + &:not(.blue, .indigo, .violet, .purple, .fuchsia) { + &:not(.pink, .rose, .grey, .black, .disabled) { + &:not(.inactive) { + border-color: currentcolor; + } + } + } + } + } + } + } +} + +.menu { + hr.padded, + .h6.padded, + h6.padded { + margin-inline-start: 20px; + } +} + +.menu--disclosure { + & > .search-container { + margin-inline: calc(var(--m) * -1); + padding: var(--m); + border-block-end: 1px solid var(--border-hairline); + } + + ul li { + &.filtered { + display: none; + } + + & > a:not(.crumb-link), + & > .menu-item, + & > .menu-option { + --focus-ring: var(--focus-ring-inner); + + &:hover { + @include mixins.disclosure-link-hover-styles; + } + } + } +} diff --git a/packages/craftcms-legacy/cp/src/css/_slideouts.scss b/packages/craftcms-legacy/cp/src/css/_slideouts.scss new file mode 100644 index 00000000000..afa92639ca1 --- /dev/null +++ b/packages/craftcms-legacy/cp/src/css/_slideouts.scss @@ -0,0 +1,303 @@ +.slideout-shade { + z-index: 100; + position: fixed; + inset-block-start: 0; + inset-inline-start: 0; + width: 100%; + height: 100%; + background: var(--shade-bg); + opacity: 0; + transition: opacity linear 250ms; + + &:not(.visible) { + display: none; + } + + &.so-visible { + opacity: 1; + } + + &.blurred { + backdrop-filter: blur(10px); + } +} + +.slideout-container { + z-index: 100; + box-sizing: border-box; + position: fixed; + inset-block-start: 0; + inset-inline-start: 0; + width: 100vw; + height: 100vh; + height: -webkit-fill-available; // h/t https://twitter.com/AllThingsSmitty/status/1254151507412496384 + pointer-events: none; + + &.so-lp { + position: fixed; + inset-block-start: var(--m); + inset-inline-start: var(--m); + width: calc(100% - var(--m) * 2); + height: calc(100vh - var(--m) * 2); + } + + body.has-debug-toolbar & { + height: calc(100vh - 42px); + } +} + +.slideout { + z-index: 100; + box-sizing: border-box; + position: absolute; + background-color: var(--modal-bg); + box-shadow: var(--modal-shadow) !important; + display: flex; + flex-direction: column; + overflow: hidden; + padding-block: 24px; + padding-inline: var(--padding); + pointer-events: all; + container-type: inline-size; + + &.so-mobile, + &.so-lp { + width: 100% !important; + height: 100% !important; + inset-inline-start: 0; + transition: inset-block-start linear 250ms; + will-change: inset-block-start; + box-shadow: none !important; + } + + &.so-mobile { + --padding: var(--m); + --neg-padding: calc(var(--m) * -1); + } + + &.so-lp { + border-radius: var(--radius-lg); + } + + &:not(.so-mobile, .so-lp) { + border-start-start-radius: var(--radius-lg); + border-start-end-radius: 0; + border-end-end-radius: 0; + border-end-start-radius: var(--radius-lg); + } + + &:not(.so-mobile, .so-lp) { + inset-block-start: 0; + width: 55%; + height: 100%; + + @media screen and (prefers-reduced-motion: no-preference) { + transition: + inset-inline-start linear 250ms, + inset-inline-end linear 250ms; + will-change: transform; + } + } + + & > .pane-header { + padding-inline: var(--padding); + z-index: 2; + border-radius: 0; + + // keep the margin-block-start as it was for wider viewports + @media only screen and (width <= 767px) { + margin-block: calc(var(--xl) * -1) var(--padding); + } + + & > .so-toolbar { + display: flex; + flex-direction: row; + align-items: center; + gap: var(--xs); + min-height: calc(44px - 16px); + + & > .pane-tabs { + width: 1px; // give other elements in the header plenty of room before the tabs take up whatever's left + flex: 1; + margin-inline-end: 0; + } + } + } + + & > .so-body { + flex: 1; + margin-block: -24px; + margin-inline: var(--neg-padding); + overflow: hidden auto; + position: relative; + + &:not(:last-child) { + margin-block-end: 0; + } + + & > h1:not(:last-child) { + padding-block-end: var(--s); + border-block-end: 1px solid var(--border-hairline); + } + + &.so-full-details, + & > .so-sidebar { + background-color: var(--gray-100) !important; + } + + &:not(.so-full-details) { + padding-block: 24px; + padding-inline: var(--padding); + } + + & > .so-sidebar, + &.so-full-details > .so-content > .details { + box-sizing: border-box; + padding-block: 0 var(--spacing); + padding-inline: var(--padding); + + // set the margin-block-start so the slideout doesn't overlap the toolbar + @media only screen and (width <= 767px) { + margin-block-start: 16px; + } + + & > .preview-thumb-container { + margin-block: 0; + margin-inline: var(--neg-padding); + height: auto; + min-height: 54px; // make room for the Preview / Edit buttons + + & + .pane-header { + border-radius: 0; + } + } + + .image-actions { + &.is-mobile { + margin-block: calc(var(--spacing) / 2) var(--spacing); + margin-inline: 0; + } + } + + & > .meta.read-only:first-child { + margin-block-start: var(--padding); + } + + & > .meta.warning { + box-shadow: none; + border-block-end: 1px solid var(--yellow-300); + } + + & > .field { + & > .input > .text.fullwidth { + border-radius: 0; + } + } + + .notes { + padding-block: var(--m); + } + } + + & > .so-sidebar { + position: absolute; + inset-block-start: 0; + width: 350px; + height: 100%; + max-width: 100%; + overflow: hidden auto; + z-index: 1001; + background: var(--pane-bg); + box-shadow: var(--pane-shadow); + + &:focus { + box-shadow: var(--focus-ring); + } + + body.ltr & { + transition: inset-inline-end linear 250ms; + } + + body.rtl & { + transition: inset-inline-start linear 250ms; + } + + & > fieldset:first-child { + margin-block-start: 24px !important; + } + } + } + + & > .so-footer { + position: relative; + display: flex; + gap: var(--s); + justify-content: flex-end; + flex-wrap: wrap; + margin-block: 0 -24px; + margin-inline: var(--neg-padding); + padding-block: 5px; + padding-inline: var(--padding); + border-block-start: var(--slideout-footer-border); + background: var(--slideout-footer-bg); + box-shadow: var(--slideout-footer-shadow); + z-index: 3; + + & > .so-notice { + display: flex; + align-items: center; + margin-inline-end: auto; + } + + & > .so-extra { + flex: 0 0 100%; + margin-block: 0; + margin-inline: var(--neg-padding); + padding-block: 0 8px; + padding-inline: var(--padding); + border-block-end: 1px solid var(--border-hairline); + } + } +} + +@container (width > 700px) { + .slideout { + &.showing-sidebar { + .so-body { + display: flex; + flex-direction: row; + padding: 0; + overflow: hidden; + + & > .so-content { + position: relative; + z-index: 2; + padding: 24px; + width: calc(100% - 350px); + height: 100%; + box-sizing: border-box; + border-inline-end: 1px solid var(--gray-200); + overflow: hidden auto; + } + + & > .so-sidebar { + position: relative; + display: block !important; + inset-block-start: auto; + inset-inline: auto !important; + height: 100%; + box-shadow: none; + } + } + } + + & > .so-footer { + & > .so-extra { + margin: 0; + padding: 0; + border: none; + flex: auto 0 1; + } + } + } +} diff --git a/packages/craftcms-legacy/cp/src/css/_tokens.scss b/packages/craftcms-legacy/cp/src/css/_tokens.scss deleted file mode 100644 index 9aea3d9a8d0..00000000000 --- a/packages/craftcms-legacy/cp/src/css/_tokens.scss +++ /dev/null @@ -1,131 +0,0 @@ -:root { - /* - Colors - */ - - /* Background */ - --bg-primary: var(--red-600); - --bg-secondary: var(--gray-500); - --bg-selection-light: var(--gray-200); - --bg-selection-dark: var(--gray-500); - --bg-error: var(--red-100); - --bg-warning: var(--amber-100); - --bg-success: var(--teal-100); - --bg-notice: var(--sky-100); - --bg-enabled: var(--teal-550); - --bg-pending: var(--orange-400); - --bg-disabled: var(--red-600); - - /* Foreground */ - --fg-subtle: var(--gray-550); - --fg-error: #d81f23; // Pretty close to --red-600 but not quite - --fg-warning: var(--amber-700); - --fg-success: var(--teal-700); - --fg-notice: var(--sky-700); - --fg-input: hsl(212deg 25% 50%); - - /* Borders */ - --border-primary: var(--red-600); - --border-secondary: var(--gray-500); - --border-hairline: rgb(from var(--gray-800) r g b / 10%); - --border-hairline-medium: #cfd8e3; - --border-hairline-dark: rgb(from hsl(210deg 10% 53%) r g b / 50%); - --border-error: var(--red-300); - --border-warning: var(--amber-300); - --border-warning-emphasis: var(--amber-600); - --border-success: var(--teal-300); - --border-notice: var(--sky-300); - - /* Focus */ - --focus-outline-light: var(--blue-300); - --focus-outline-medium: var(--focus-ring-color, #0284c7); - --focus-outline-dark: #0f74b1; - - /* - Focus Rings - */ - --focus-ring-alpha: 0.85; - --focus-ring-light: - 0 0 0 1px hsl(from var(--focus-outline-light) h s calc(l + 10)), - 0 0 0 3px rgb(from var(--focus-outline-light) r g b / 97%); - --focus-ring-medium: - 0 0 0 1px hsl(from var(--focus-outline-medium) h s calc(l + 10)), - 0 0 0 3px rgb(from var(--focus-outline-medium) r g b / 97%); - --focus-ring-dark: - 0 0 0 1px hsl(from var(--focus-outline-dark) h s calc(l + 20)), - 0 0 0 3px rgb(from var(--focus-outline-dark) r g b / 97%); - --focus-ring: var(--focus-ring-medium); - --focus-ring-outset: - 0 0 0 1px currentcolor, 0 0 0 3px var(--focus-outline-medium), - 0 0 6px 2px - hsl(from var(--focus-outline-medium) h s l / var(--focus-ring-alpha)); - --focus-ring-inner: - inset 0 0 0 1px var(--focus-outline-medium), - inset 0 0 0 3px - hsl(from var(--focus-outline-medium) h s l / var(--focus-ring-alpha)); - --focus-ring-inner-light: - inset 0 0 0 1px var(--focus-outline-light), - inset 0 0 0 3px - hsl(from var(--focus-outline-light) h s l / var(--focus-ring-alpha)); - - /* - Border Radius - */ - --radius-sm: 3px; - --radius-md: 4px; - --radius-lg: 5px; - --radius-full: calc(infinity * 1px); - --radius-circle: 50%; - - /* - Sizing - */ - --size-touch-target: calc(24rem / 16); - --size-icon: 1rem; - --size-line-height: 1.42em; - - /* - Spacing - */ - --2xs: calc(2rem / 16); - --xs: calc(4rem / 16); - --s: calc(8rem / 16); - --m: calc(14rem / 16); - --l: calc(18rem / 16); - --xl: calc(24rem / 16); - --padding: var(--xl); - --neg-padding: calc(var(--xl) * -1); - - /* - Components - */ - - /* UI Controls */ - --ui-control-fg: var(--fg-subtle); - --ui-control-fg-hover: var(--gray-600); - --ui-control-fg-active: var(--gray-700); - --ui-control-bg: rgb(from var(--fg-input) r g b / 25%); - --ui-control-bg-static: var(--ui-control-bg); - --ui-control-bg-hover: rgb(from var(--fg-input) r g b / 30%); - --ui-control-bg-active: rgb(from var(--fg-input) r g b / 50%); - --ui-control-border: var(--ui-control-fg); - --ui-control-radius: var(--radius-lg); - --ui-control-height: calc(34rem / 16); - --ui-control-height-small: calc(30rem / 16); - - /* Sidebars */ - --sidebar-bg: var(--gray-150); - - /* Nav Item */ - --nav-item-indicator-size: var(--xs); - --nav-item-badge-bg: var(--fg-subtle); - --nav-item-badge-fg: var(--white); - --nav-item-fg-active: var(--text-color); - --nav-item-fg-hover: var(--text-color); - --nav-item-bg-active: var(--gray-200); - --nav-item-bg-hover: var(--gray-200); - --nav-item-prefix-width: calc(30rem / 16); - --nav-item-prefix-ratio: 1; - --nav-item-trigger-size: var(--touch-target-size); - --nav-item-gutter-width: calc(10rem / 16); // Left and right padding -} diff --git a/packages/craftcms-legacy/cp/src/css/craft.scss b/packages/craftcms-legacy/cp/src/css/craft.scss index 6f4edc78fb1..ff419da226b 100644 --- a/packages/craftcms-legacy/cp/src/css/craft.scss +++ b/packages/craftcms-legacy/cp/src/css/craft.scss @@ -1,20 +1,25 @@ @charset "utf-8"; -@use 'color-palette'; -@use 'tokens'; -@use 'variables'; -@use 'main'; -@use 'cp'; -@use 'range'; -@use 'global-sidebar'; -@use 'craft-disclosure'; -@use 'craft-spinner'; -@use 'craft-tooltip'; -@use 'preview'; -@use 'login'; -@use 'entry-type-select'; -@use 'fld'; -@use 'grouped-entry-type-select'; -@use 'image_editor'; -@use 'shame'; -@use 'debug_toolbar'; +@import 'compat'; +@import 'slideouts'; +@import 'menu'; +@import 'hud'; +@import 'icon-picker'; +@import 'login'; + +.cp-legacy { + @import 'main'; + @import 'cp'; + @import 'range'; + @import 'global-sidebar'; + @import 'craft-disclosure'; + @import 'craft-spinner'; + @import 'craft-tooltip'; + @import 'preview'; + @import 'entry-type-select'; + @import 'fld'; + @import 'grouped-entry-type-select'; + @import 'image_editor'; + @import 'shame'; + @import 'debug_toolbar'; +} diff --git a/packages/craftcms-legacy/cp/src/js/Craft.js b/packages/craftcms-legacy/cp/src/js/Craft.js index aad85f7e6b8..17a3ed3fe14 100644 --- a/packages/craftcms-legacy/cp/src/js/Craft.js +++ b/packages/craftcms-legacy/cp/src/js/Craft.js @@ -2117,21 +2117,22 @@ $.extend(Craft, { * Swap any instruction text with info icons but avoid those with the class * visually-hidden as those have already been swapped * This needs to happen before the `infoicon` method - */ + * + * The primary place this happens is in the advanced settings of the link field + * */ $( '.field.info-icon-instructions > .instructions, #details .meta > .field > .instructions', $container ) - .not('.visually-hidden') + .not('.visually-hidden,.sr-only') .each(function () { const $instructions = $(this); const $label = $instructions.siblings('.heading').find('label'); - $('
', { - class: 'info', + $('', { html: $instructions.children().html(), }).appendTo($label); // Keep the original element around in case an aria-describedby attribute is referencing it - $instructions.addClass('visually-hidden'); + $instructions.addClass('sr-only'); }); $('.info', $container).infoicon(); diff --git a/packages/craftcms-legacy/cp/src/js/FieldLayoutDesigner.js b/packages/craftcms-legacy/cp/src/js/FieldLayoutDesigner.js index de839c21e44..cc9a7f4f81e 100644 --- a/packages/craftcms-legacy/cp/src/js/FieldLayoutDesigner.js +++ b/packages/craftcms-legacy/cp/src/js/FieldLayoutDesigner.js @@ -503,7 +503,7 @@ Craft.FieldLayoutDesigner.Tab = Garnish.Base.extend({ this.$addBtn = $tabContent.children('.fld-add-btn'); const hud = new Garnish.HUD(this.$addBtn, { - hudClass: 'hud fld-library-hud', + hudClass: 'hud fld-library-hud cp-legacy', listenToMainResize: false, showOnInit: false, orientations: ['right', 'bottom', 'left'], @@ -1896,9 +1896,19 @@ Craft.FieldLayoutDesigner.CardViewDesigner = Garnish.Base.extend({ this.$libraryContainer = this.$container.find( '.cvd-library .checkbox-select' ); + this.sortableCheckboxSelect = this.$libraryContainer.data( 'sortableCheckboxSelect' ); + + // If the checkboxes haven't been initialized yet, do that. + if (!this.sortableCheckboxSelect) { + console.trace('Not initialized'); + this.sortableCheckboxSelect = new Craft.SortableCheckboxSelect( + this.$libraryContainer + ); + } + this.$thumbManagementContainer = this.$container.find('.thumb-management'); this.alwaysShowThumbAlignmentBtns = designer.settings.alwaysShowThumbAlignmentBtns; diff --git a/packages/craftcms-legacy/cp/src/js/IconPicker.js b/packages/craftcms-legacy/cp/src/js/IconPicker.js index fe29a022d57..22500956b2e 100644 --- a/packages/craftcms-legacy/cp/src/js/IconPicker.js +++ b/packages/craftcms-legacy/cp/src/js/IconPicker.js @@ -56,7 +56,7 @@ Craft.IconPicker = Craft.BaseInputGenerator.extend( }, createModal() { - const $container = $('