Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
61e5139
Start updating landmarks and adding skip links
gcamacho079 Mar 2, 2026
51316d9
Update skip link styles and add global skip link modifier
gcamacho079 Mar 2, 2026
e50a987
Refactor where nav element is in secondary sidebar
gcamacho079 Mar 2, 2026
4e8e99f
Use computed prop for skipLinks inside of AppLayout
gcamacho079 Mar 3, 2026
24161ba
Update label on primary nav
gcamacho079 Mar 3, 2026
c807bdc
Use aria-label instead
gcamacho079 Mar 3, 2026
21e8f2a
Add ID back to second nav
gcamacho079 Mar 3, 2026
dc62882
Fix all usages of translate
gcamacho079 Mar 3, 2026
24981d0
Add content pane skip link
gcamacho079 Mar 3, 2026
f1b57c8
Move breadcrumbs out of main container, update header top-level to be…
gcamacho079 Mar 3, 2026
c9e555f
Quick build
gcamacho079 Mar 3, 2026
86d5077
Add a key to the skip link v-for
gcamacho079 Mar 4, 2026
82f6923
Remove default crumb prop from AppLayout component
gcamacho079 Mar 4, 2026
b36a777
Merge branch '6.x' of github.com:craftcms/cms into a11y/bypass-blocks
brianjhanson Mar 5, 2026
a74f0d5
Merge branch '6.x' of github.com:craftcms/cms into a11y/bypass-blocks
brianjhanson Mar 5, 2026
995dd3a
Implement an action system via the plugins index page
brianjhanson Apr 23, 2026
4c86085
Fix type error
brianjhanson Apr 23, 2026
ccd5311
Add server-driven menu actions documentation
brianjhanson Apr 24, 2026
7d18115
Merge branch 'feature/plugins-page' of github.com:craftcms/cms into f…
brianjhanson Apr 24, 2026
6e50eba
Add copy package name action
brianjhanson Apr 24, 2026
8143dd8
Merge branch '6.x' of github.com:craftcms/cms into a11y/bypass-blocks
brianjhanson Apr 27, 2026
eb0053c
Merge pull request #18499 from craftcms/a11y/bypass-blocks
brianjhanson Apr 27, 2026
f2c5fb7
Merge branch '6.x' of github.com:craftcms/cms into feature/actions
brianjhanson Apr 27, 2026
e0e945e
Round out the plugin page functionality
brianjhanson Apr 27, 2026
4b050f5
Cleanup
brianjhanson Apr 27, 2026
acaae56
Merge branch 'feature/inertia-ui' of github.com:craftcms/cms into fea…
brianjhanson Apr 27, 2026
ef313e1
Fix legacy asset registration
brianjhanson Apr 28, 2026
2727ab9
Fix element chip reordering
brianjhanson Apr 28, 2026
3623a65
Fix logi
brianjhanson Apr 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
360 changes: 360 additions & 0 deletions .storybook/inertia-mock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,360 @@
import {defineComponent, h, 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',
handle: 'pro',
value: 2,
},
},
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,
};

/**
* Reactive page state that can be modified by stories
*/
export const pageState = reactive({
props: {...defaultPageProps, errors: {}, deferred: {}},
url: '/',
component: 'Story',
version: '1',
clearHistory: false,
encryptHistory: false,
flash: {},
rememberedState: {},
});

/**
* Reset page props to defaults, optionally merging with overrides
*/
export function setPageProps(overrides = {}) {
pageState.props = deepMerge({...defaultPageProps}, overrides);
}

/**
* Deep merge utility
*/
function deepMerge(target, source) {
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, sourceValue);
} else {
result[key] = sourceValue;
}
}
}

return result;
}

/**
* Mock usePage composable
*/
export function usePage() {
return pageState;
}

/**
* Mock router for Inertia
*/
export const router = {
visit(url, options) {
console.log('[Storybook] router.visit:', url, options);
},
get(url, data, options) {
console.log('[Storybook] router.get:', url, data, options);
},
post(url, data, options) {
console.log('[Storybook] router.post:', url, data, options);
},
put(url, data, options) {
console.log('[Storybook] router.put:', url, data, options);
},
patch(url, data, options) {
console.log('[Storybook] router.patch:', url, data, options);
},
delete(url, options) {
console.log('[Storybook] router.delete:', url, options);
},
reload(options) {
console.log('[Storybook] router.reload:', options);
},
replace(url, options) {
console.log('[Storybook] router.replace:', url, options);
},
on(event, callback) {
console.log('[Storybook] router.on:', event);
return () => {};
},
};

/**
* Mock useForm composable
*/
export function useForm(rememberKeyOrData, maybeData) {
const initialValues =
typeof rememberKeyOrData === 'string' ? maybeData : rememberKeyOrData;
const formData = reactive({...initialValues});
const errors = {};

const formProps = {
errors,
hasErrors: false,
processing: false,
progress: null,
wasSuccessful: false,
recentlySuccessful: false,
isDirty: false,
data: () => formData,
transform: () => formProps,
defaults: () => formProps,
reset(...fields) {
if (fields.length === 0) {
Object.assign(formData, initialValues);
} else {
fields.forEach((field) => {
formData[field] = initialValues[field];
});
}
return formProps;
},
clearErrors(...fields) {
if (fields.length === 0) {
Object.keys(errors).forEach((key) => delete errors[key]);
} else {
fields.forEach((field) => delete errors[field]);
}
formProps.hasErrors = Object.keys(errors).length > 0;
return formProps;
},
resetAndClearErrors(...fields) {
formProps.reset(...fields);
formProps.clearErrors(...fields);
return formProps;
},
setError(fieldOrErrors, maybeValue) {
if (typeof fieldOrErrors === 'string') {
errors[fieldOrErrors] = maybeValue;
} else {
Object.assign(errors, fieldOrErrors);
}
formProps.hasErrors = true;
return formProps;
},
submit(...args) {
console.log('[Storybook] form.submit:', ...args);
},
get(url, options) {
console.log('[Storybook] form.get:', url, options);
},
post(url, options) {
console.log('[Storybook] form.post:', url, options);
},
put(url, options) {
console.log('[Storybook] form.put:', url, options);
},
patch(url, options) {
console.log('[Storybook] form.patch:', url, options);
},
delete(url, options) {
console.log('[Storybook] form.delete:', url, options);
},
cancel() {
console.log('[Storybook] form.cancel');
},
dontRemember: () => formProps,
optimistic: () => formProps,
withPrecognition: () => formProps,
};

return reactive({...formData, ...formProps});
}

/**
* Mock Head component
*/
export const Head = defineComponent({
name: 'Head',
props: {
title: String,
},
setup() {
return () => null;
},
});

/**
* Mock Link component
*/
export const Link = defineComponent({
name: 'Link',
props: {
href: {
type: String,
required: true,
},
method: {
type: String,
default: 'get',
},
data: Object,
replace: Boolean,
preserveScroll: Boolean,
preserveState: Boolean,
only: Array,
headers: Object,
as: {
type: String,
default: 'a',
},
},
setup(props, {slots, attrs}) {
return () =>
h(
props.as,
{
...attrs,
href: props.href,
onClick: (e) => {
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,
default: 'post',
},
action: String,
data: Object,
preserveScroll: Boolean,
preserveState: Boolean,
only: Array,
headers: Object,
},
setup(props, {slots, attrs}) {
const formState = reactive({
processing: false,
errors: {},
hasErrors: false,
wasSuccessful: false,
recentlySuccessful: false,
});

return () =>
h(
'form',
{
...attrs,
action: props.action,
method: props.method === 'get' ? 'get' : 'post',
onSubmit: (e) => {
e.preventDefault();
console.log(
'[Storybook] Form submitted:',
props.action,
props.method,
props.data
);
},
},
slots.default?.(formState)
);
},
});

/**
* Mock Deferred component - renders children immediately in Storybook
*/
export const Deferred = defineComponent({
name: 'Deferred',
props: {
data: {
type: [String, Array],
required: true,
},
},
setup(props, {slots}) {
return () => slots.default?.();
},
});

/**
* Mock createInertiaApp - not typically used in Storybook stories
*/
export function createInertiaApp(options) {
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.config.globalProperties.$page = pageState;
app.config.globalProperties.$inertia = router;

app.component('Head', Head);
app.component('Link', Link);
app.component('InertiaLink', Link);

app.provide('$inertia', router);
app.provide('$page', pageState);
}
Loading
Loading