Skip to content

Commit 8009732

Browse files
committed
Feature parity with legacy code
1 parent 831bd55 commit 8009732

13 files changed

Lines changed: 1217 additions & 115 deletions

File tree

.storybook/inertia-mock.ts

Lines changed: 437 additions & 0 deletions
Large diffs are not rendered by default.

.storybook/main.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type {StorybookConfig} from '@storybook/vue3-vite';
22
import {dirname, join} from 'path';
3-
import {mergeConfig} from 'vite';
43
import vue from '@vitejs/plugin-vue';
54
import tailwindcss from '@tailwindcss/vite';
65

@@ -21,28 +20,47 @@ const config: StorybookConfig = {
2120
],
2221
framework: {
2322
name: getAbsolutePath('@storybook/vue3-vite') as '@storybook/vue3-vite',
24-
options: {},
23+
options: {
24+
docgen: 'vue-component-meta',
25+
},
2526
},
2627
viteFinal(config) {
27-
return mergeConfig(config, {
28+
// Storybook's vue3-vite framework adds its own Vue plugin with default options.
29+
// We need to configure `isCustomElement` so Vue treats `craft-*` tags as web
30+
// components (from @craftcms/cp) rather than trying to resolve them as Vue
31+
// components. Since Vite's mergeConfig doesn't deep-merge plugin options,
32+
// we remove Storybook's Vue plugin and add our own with the correct config.
33+
const filteredPlugins = (config.plugins || []).flat().filter((plugin) => {
34+
if (plugin && typeof plugin === 'object' && 'name' in plugin) {
35+
return plugin.name !== 'vite:vue';
36+
}
37+
return true;
38+
});
39+
40+
return {
41+
...config,
2842
plugins: [
43+
...filteredPlugins,
2944
tailwindcss(),
3045
vue({
3146
template: {
3247
compilerOptions: {
33-
// Treat craft-* tags as custom elements (web components from @craftcms/cp)
3448
isCustomElement: (tag) => tag.startsWith('craft-'),
3549
},
3650
},
3751
}),
3852
],
3953
resolve: {
54+
...config.resolve,
4055
alias: {
56+
...(config.resolve?.alias || {}),
4157
'@': join(__dirname, '../resources/js'),
4258
vue: 'vue/dist/vue.esm-bundler.js',
59+
// Mock Inertia for Storybook
60+
'@inertiajs/vue3': join(__dirname, 'inertia-mock.ts'),
4361
},
4462
},
45-
});
63+
};
4664
},
4765
};
4866

.storybook/preview.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,22 @@
11
import type {Preview} from '@storybook/vue3';
2+
import {setup} from '@storybook/vue3';
23
import {withThemeByDataAttribute} from '@storybook/addon-themes';
4+
import '@craftcms/cp';
35
import '../resources/css/cp.css';
46
import './preview.css';
7+
import {installInertiaMock, type PageProps, setPageProps} from './inertia-mock';
8+
9+
// Install the Inertia mock globally
10+
setup((app) => {
11+
installInertiaMock(app);
12+
});
13+
14+
// Declare module augmentation for Storybook parameters
15+
declare module '@storybook/vue3' {
16+
interface Parameters {
17+
inertia?: Partial<PageProps>;
18+
}
19+
}
520

621
const preview: Preview = {
722
parameters: {
@@ -25,6 +40,13 @@ const preview: Preview = {
2540
},
2641
},
2742
decorators: [
43+
// Inertia page props decorator - must come before theme decorator
44+
(story, context) => {
45+
// Reset and apply any story-specific page props
46+
const inertiaProps = context.parameters.inertia || {};
47+
setPageProps(inertiaProps);
48+
return story();
49+
},
2850
withThemeByDataAttribute({
2951
themes: {
3052
light: 'light',

packages/craftcms-cp/src/styles/shared/base.css

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,7 @@ Modify with `cp-table--auto` to apply table-layout: auto
182182
padding-inline: var(--_cell-spacing-inline);
183183
position: relative;
184184

185-
&:has(textarea),
186-
&:has(input:not([type='checkbox']):not([type='radio'])) {
185+
&:has(.cp-table-input:not([type='checkbox']):not([type='radio'])) {
187186
padding: 0;
188187
}
189188
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import type {
2+
ComponentPropsAndSlots,
3+
Meta,
4+
StoryObj,
5+
} from '@storybook/vue3-vite';
6+
import {createPlugin} from '@/fixtures/plugins';
7+
8+
import PluginDetails from './PluginDetails.vue';
9+
10+
const meta = {
11+
component: PluginDetails,
12+
} satisfies Meta<typeof PluginDetails>;
13+
14+
export default meta;
15+
type Story = StoryObj<typeof meta>;
16+
17+
function render(args: ComponentPropsAndSlots<typeof PluginDetails>) {
18+
return {
19+
components: {PluginDetails},
20+
setup() {
21+
return {args};
22+
},
23+
template: '<PluginDetails v-bind="args"/>',
24+
};
25+
}
26+
27+
/*
28+
*👇 Render functions are a framework specific feature to allow you control on how the component renders.
29+
* See https://storybook.js.org/docs/api/csf
30+
* to learn how to use render functions.
31+
*/
32+
export const Default: Story = {
33+
render,
34+
args: {
35+
plugin: createPlugin(),
36+
},
37+
};
38+
39+
export const ValidLicense: Story = {
40+
render,
41+
args: {
42+
plugin: createPlugin({
43+
licenseKeyStatus: 'valid',
44+
licenseKey: 'ABCDEFGHIJKLMNOP',
45+
licensedEdition: 'pro',
46+
}),
47+
},
48+
};
49+
50+
export const TrialLicense: Story = {
51+
render,
52+
args: {
53+
plugin: createPlugin({
54+
isTrial: true,
55+
licenseKeyStatus: 'trial',
56+
licenseKey: 'ABCDEFGHIJKLMNOP',
57+
licenseIssues: ['no_trials'],
58+
licensedEdition: 'pro',
59+
}),
60+
},
61+
};
62+
63+
export const InvalidLicense: Story = {
64+
render,
65+
args: {
66+
plugin: createPlugin({
67+
licenseKeyStatus: 'invalid',
68+
licenseKey: 'ABCDEFGHIJKLMNOP',
69+
licenseIssues: ['invalid'],
70+
licensedEdition: 'lite',
71+
}),
72+
},
73+
};
74+
75+
export const AllLicenseIssues: Story = {
76+
render,
77+
args: {
78+
plugin: createPlugin({
79+
licenseKey: 'ABCDEFGHIJKLMNOP',
80+
licenseKeyStatus: 'trial',
81+
licensedEdition: 'lite',
82+
licenseIssues: [
83+
'wrong_edition',
84+
'no_trials',
85+
'mismatched',
86+
'astray',
87+
'required',
88+
'any',
89+
],
90+
}),
91+
},
92+
};

resources/js/components/Plugins/PluginDetails.vue

Lines changed: 70 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import PluginEdition from '@/components/Plugins/PluginEdition.vue';
77
import CpLink from '@/components/CpLink.vue';
88
import CraftInput from '@craftcms/cp/vue/CraftInput.vue';
9+
import PluginLicenseStatusIcon from '@/components/Plugins/PluginLicenseStatusIcon.vue';
910
1011
const props = defineProps<{
1112
plugin: PluginInfo;
@@ -53,7 +54,16 @@
5354

5455
<template>
5556
<div class="cp-plugin">
56-
<div class="cp-plugin__icon" v-html="plugin.iconSvg"></div>
57+
<div class="cp-plugin__icon">
58+
<div class="relative">
59+
<span v-html="plugin.iconSvg"></span>
60+
<PluginLicenseStatusIcon
61+
v-if="plugin.licenseKeyStatus === 'valid' || licenseIssues.length > 0"
62+
class="license-key-status"
63+
:status="plugin.licenseIssues.length === 0 ? 'valid' : 'invalid'"
64+
/>
65+
</div>
66+
</div>
5767
<div>
5868
<div class="flex gap-2 items-baseline mb-1">
5969
<h2>{{ plugin.name }}</h2>
@@ -81,56 +91,87 @@
8191
<div>
8292
<ul v-if="plugin.links.length > 0" class="flex gap-3 items-base">
8393
<li v-for="link in plugin.links">
84-
<a :href="link.href" target="_blank" rel="noopener" class="flex gap-1 items-center">
94+
<a
95+
:href="link.href"
96+
target="_blank"
97+
rel="noopener"
98+
class="flex gap-1 items-center"
99+
>
85100
<craft-icon v-if="link.icon" :name="link.icon"></craft-icon>
86101
{{ link.text }}
87102
</a>
88103
</li>
89104
</ul>
90105
</div>
91106

92-
<div class="flex gap-2 items-center my-4" v-if="showLicenseKey">
93-
<craft-input
94-
:value="plugin.licenseKey"
95-
class="font-mono"
96-
readonly
97-
:style="{
98-
width: `${plugin.licenseKey.length + 6}ch`,
99-
}"
107+
<div class="my-4">
108+
<div
109+
class="flex gap-2 items-center mb-1 max-w-[20rem]"
110+
v-if="showLicenseKey"
100111
>
101-
<craft-copy-button
102-
slot="suffix"
112+
<craft-input
103113
:value="plugin.licenseKey"
104-
></craft-copy-button>
105-
</craft-input>
106-
107-
<template v-if="!page.props.readOnly && plugin.buyUrl">
108-
<CpLink
109-
appearance="button"
110-
:inertia="false"
111-
v-if="plugin.licenseKeyStatus === 'trial'"
112-
:href="plugin.buyUrl"
113-
:variant="plugin.licenseIssues.length > 0 ? 'primary' : 'default'"
114-
>{{ t('Buy now') }}</CpLink
114+
class="font-mono flex-1"
115+
:label="t('License Key')"
116+
label-sr-only
117+
readonly
118+
:style="{
119+
width: `${plugin.licenseKey.length + 6}ch`,
120+
}"
115121
>
122+
<craft-copy-button
123+
slot="suffix"
124+
:value="plugin.licenseKey"
125+
></craft-copy-button>
126+
</craft-input>
127+
128+
<template
129+
v-if="
130+
!page.props.readOnly &&
131+
plugin.buyUrl &&
132+
plugin.licenseKeyStatus === 'trial'
133+
"
134+
>
135+
<CpLink
136+
appearance="button"
137+
:inertia="false"
138+
:href="plugin.buyUrl"
139+
:variant="plugin.licenseIssues.length > 0 ? 'primary' : 'default'"
140+
>{{ t('Buy now') }}</CpLink
141+
>
142+
</template>
143+
</div>
144+
145+
<template v-for="issue in licenseIssues">
146+
<craft-callout
147+
variant="danger"
148+
appearance="plain"
149+
class="p-0"
150+
v-html="issue"
151+
></craft-callout>
116152
</template>
117153
</div>
118-
119-
<template v-for="issue in licenseIssues">
120-
<div v-html="issue"></div>
121-
</template>
122154
</div>
123155
</div>
124156
</template>
125157

126158
<style scoped lang="scss">
127159
.cp-plugin {
128160
display: grid;
129-
grid-template-columns: auto 1fr;
130-
gap: var(--c-spacing-md);
161+
grid-template-columns: 56px 1fr;
162+
gap: var(--c-spacing-lg);
131163
padding: var(--c-spacing-md);
132164
}
133165
166+
.license-key-status {
167+
display: block;
168+
position: absolute;
169+
inset-inline-end: calc(2rem / 16 * -1);
170+
inset-block-end: calc(5rem / 16);
171+
width: calc(20rem / 16);
172+
height: calc(20rem / 16);
173+
}
174+
134175
.cp-plugin__icon :deep(svg) {
135176
width: 56px;
136177
height: 56px;

0 commit comments

Comments
 (0)