Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 11 additions & 12 deletions resources/css/components/publish.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@
}

.form-group {
grid-column: span var(--col-span, var(--grid-columns));
/* Stacked, until the panel is wide enough for the configured widths to make sense. */
grid-column: span var(--grid-columns);

@variant @lg/panel {
grid-column: span var(--col-span, var(--grid-columns));
}
}

.spacer-fieldtype {
Expand All @@ -26,21 +31,15 @@
}
}

@variant @lg/panel {
.field-w-25 { --col-span: calc(var(--grid-columns) * 0.25); }
.field-w-33 { --col-span: calc(var(--grid-columns) * 0.333); }
.field-w-50 { --col-span: calc(var(--grid-columns) * 0.5); }
.field-w-66 { --col-span: calc(var(--grid-columns) * 0.666); }
.field-w-75 { --col-span: calc(var(--grid-columns) * 0.75); }
.field-w-undefined, .field-w-100 { --col-span: calc(var(--grid-columns) * 1); }
}


/* GROUP PUBLISH COMPONENT / FIELD GRID (e.g. BLUEPRINT)
=================================================== */
.field-grid {
> * {
grid-column: span var(--col-span, var(--grid-columns));
grid-column: span var(--grid-columns);

@variant @lg/panel {
grid-column: span var(--col-span, var(--grid-columns));
}
}
}

Expand Down
22 changes: 22 additions & 0 deletions resources/css/components/widgets.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/* GROUP DASHBOARD WIDGETS
=================================================== */

.widgets {
display: grid;
--grid-columns: 12;
grid-template-columns: repeat(var(--grid-columns), 1fr);
@apply gap-6;

> * {
/*
* Stacked, until the dashboard is wide enough for the arrangement to
* hold up. One threshold for every widget, so there are only ever two
* layouts: the one that was configured, or everything in order.
*/
grid-column: span var(--grid-columns);

@variant @2xl/widgets {
grid-column: span var(--col-span, var(--grid-columns));
}
}
}
50 changes: 50 additions & 0 deletions resources/css/components/widths.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/* GROUP WIDTHS
=================================================== */

/*
* A configured width resolves to a span of a twelve column grid.
*
* Consumed by the publish form grid (publish.css) and the dashboard widget grid
* (widgets.css). Each of those decides for itself at what container width it
* stops honouring the span and stacks everything instead, so these rules are
* unconditional.
*
* Spans and percentages are shared. The size keywords are widgets only, and are
* deliberately not given .field-w- equivalents.
*
* Spans are written as a fraction of --grid-columns rather than as plain numbers
* so that a grid redefining it continues to opt out of column spanning entirely.
* The asset editor does this by setting it to `none`.
*/

.field-w-1, .widget-w-1 { --col-span: calc(var(--grid-columns) * 1 / 12); }
.field-w-2, .widget-w-2 { --col-span: calc(var(--grid-columns) * 2 / 12); }
.field-w-3, .widget-w-3 { --col-span: calc(var(--grid-columns) * 3 / 12); }
.field-w-4, .widget-w-4 { --col-span: calc(var(--grid-columns) * 4 / 12); }
.field-w-5, .widget-w-5 { --col-span: calc(var(--grid-columns) * 5 / 12); }
.field-w-6, .widget-w-6 { --col-span: calc(var(--grid-columns) * 6 / 12); }
.field-w-7, .widget-w-7 { --col-span: calc(var(--grid-columns) * 7 / 12); }
.field-w-8, .widget-w-8 { --col-span: calc(var(--grid-columns) * 8 / 12); }
.field-w-9, .widget-w-9 { --col-span: calc(var(--grid-columns) * 9 / 12); }
.field-w-10, .widget-w-10 { --col-span: calc(var(--grid-columns) * 10 / 12); }
.field-w-11, .widget-w-11 { --col-span: calc(var(--grid-columns) * 11 / 12); }
.field-w-12, .widget-w-12 { --col-span: calc(var(--grid-columns) * 12 / 12); }

/* Percentages. The widths available before spans, and still what the width
selector writes. They were always exact twelfths. */
.field-w-25, .widget-w-25 { --col-span: calc(var(--grid-columns) * 3 / 12); }
.field-w-33, .widget-w-33 { --col-span: calc(var(--grid-columns) * 4 / 12); }
.field-w-50, .widget-w-50 { --col-span: calc(var(--grid-columns) * 6 / 12); }
.field-w-66, .widget-w-66 { --col-span: calc(var(--grid-columns) * 8 / 12); }
.field-w-75, .widget-w-75 { --col-span: calc(var(--grid-columns) * 9 / 12); }
.field-w-100, .widget-w-100 { --col-span: calc(var(--grid-columns) * 12 / 12); }

/* Size keywords. Widgets only. Fields have never accepted these. */
.widget-w-sm { --col-span: calc(var(--grid-columns) * 4 / 12); }
.widget-w-md { --col-span: calc(var(--grid-columns) * 6 / 12); }
.widget-w-lg { --col-span: calc(var(--grid-columns) * 9 / 12); }
.widget-w-full { --col-span: calc(var(--grid-columns) * 12 / 12); }

/* No width configured. Needs a rule of its own rather than relying on the
var() fallback, because --col-span inherits. */
.field-w-undefined, .widget-w-undefined { --col-span: calc(var(--grid-columns) * 12 / 12); }
2 changes: 2 additions & 0 deletions resources/css/cp.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
@import './components/stacks.css';
@import './components/tabs.css';
@import './components/tooltips.css';
@import './components/widgets.css';
@import './components/widths.css';
@import './components/wizard.css';

/* Fieldtypes */
Expand Down
9 changes: 9 additions & 0 deletions resources/js/bootstrap/globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ export function clone(value) {
return JSON.parse(JSON.stringify(value));
}

/**
* @deprecated Widths are resolved by the stylesheet now. Emit `field-w-${width}`
* or `widget-w-${width}` instead. Will be removed in a future major version.
*/
export function tailwind_width_class(width) {
const widths = {
25: 'w-full @lg:w-1/4',
Expand All @@ -62,6 +66,11 @@ export function tailwind_width_class(width) {
return `${widths[width] || 'w-full'}`;
}

/**
* @deprecated Widths are resolved by the stylesheet now, which understands column
* spans as well as these six percentages. Emit `field-w-${width}` instead.
* Will be removed in a future major version.
*/
export function field_width_class(width) {
const widths = {
25: 'field-w-25',
Expand Down
2 changes: 1 addition & 1 deletion resources/js/components/blueprints/RegularField.vue
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export default {
},

widthClass() {
return `${field_width_class(this.width)}`;
return `field-w-${this.width}`;
},

localizable: {
Expand Down
24 changes: 17 additions & 7 deletions resources/js/components/fields/WidthSelector.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script setup>
import { ref, computed } from 'vue'
import { cva } from 'cva'
import { GRID_COLUMNS, widthToColumnSpan, widthToPercentage } from '@/util/width.js'

const props = defineProps({
modelValue: Number,
Expand All @@ -13,7 +14,14 @@ const emit = defineEmits(['update:model-value'])

const isHovering = ref(false)
const hoveringOver = ref(null)
const widths = ref(props.initialWidths ?? [25, 33, 50, 66, 75, 100])
// Compared on resolved spans rather than raw values, so a width the stops don't
// offer (a hand written span of 6, say) still fills the selector to the right place.
const widths = computed(() =>
(props.initialWidths ?? [25, 33, 50, 66, 75, 100]).map((value) => ({
value,
span: widthToColumnSpan(value),
})),
)

const selected = computed(() => {
if (isHovering.value) {
Expand All @@ -22,6 +30,8 @@ const selected = computed(() => {
return props.modelValue
})

const selectedSpan = computed(() => widthToColumnSpan(selected.value))

const wrapperClasses = cva({
base: 'relative text-gray-600 dark:text-gray-400 font-mono antialiased bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 with-contrast:border-gray-500 overflow-hidden flex cursor-pointer',
variants: {
Expand Down Expand Up @@ -58,14 +68,14 @@ const sizerClasses = cva({
<div class="flex w-full">
<div
v-for="width in widths"
:key="width"
@mouseenter.stop="hoveringOver = width"
@click="$emit('update:model-value', width)"
:key="width.value"
@mouseenter.stop="hoveringOver = width.value"
@click="$emit('update:model-value', width.value)"
:class="sizerClasses"
:data-state="selected >= width ? 'selected' : 'unselected'"
:data-last="selected === width && width !== 100"
:data-state="selectedSpan >= width.span ? 'selected' : 'unselected'"
:data-last="selectedSpan === width.span && width.span !== GRID_COLUMNS"
/>
</div>
<div class="pointer-events-none absolute inset-0 z-10 flex w-full items-center justify-center text-center font-medium text-gray-900 dark:text-gray-300">{{ selected }}%</div>
<div class="pointer-events-none absolute inset-0 z-10 flex w-full items-center justify-center text-center font-medium text-gray-900 dark:text-gray-300">{{ widthToPercentage(selected) }}%</div>
</div>
</template>
29 changes: 3 additions & 26 deletions resources/js/pages/Dashboard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,7 @@ const props = defineProps({
if (props.widgets.length === 0) useArchitecturalBackground();

function classes(widget) {
return `${widget.classes} ${tailwindWidthClass(widget.width)}`;
}

function tailwindWidthClass(width) {
const sizes = {
sm: 'w-full @2xl:w-1/2 @4xl:w-1/3 @7xl:w-1/4',
md: 'w-full @2xl:w-1/2 @4xl:w-1/2 @7xl:w-1/3',
lg: 'w-full @2xl:w-full @4xl:w-2/3 @7xl:w-3/4',
full: 'w-full',
};

// For backward compatibility, map old numeric widths to new sizes
const legacyMap = {
25: 'sm',
33: 'sm',
50: 'md',
66: 'md',
75: 'lg',
100: 'full'
};

const size = typeof width === 'number' ? (legacyMap[width] ?? 'full') : width;

return sizes[size] ?? sizes.md;
return `${widget.classes} widget-w-${widget.width}`;
}
</script>

Expand All @@ -48,10 +25,10 @@ function tailwindWidthClass(width) {
<template v-if="widgets.length">
<ui-header :title="__('Dashboard')" icon="dashboard" />

<div class="widgets @container/widgets flex flex-wrap gap-y-6 -mx-2 sm:-mx-3">
<div class="widgets @container/widgets">
<div
v-for="widget in widgets"
class="px-3 starting-style-transition"
class="starting-style-transition"
:class="classes(widget)"
>
<component v-if="widget.component" :is="widget.component.name" v-bind="widget.component.props" />
Expand Down
45 changes: 45 additions & 0 deletions resources/js/tests/components/WidthSelector.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { mount } from '@vue/test-utils';
import { expect, test } from 'vitest';
import WidthSelector from '@/components/fields/WidthSelector.vue';

function states(wrapper) {
return wrapper.findAll('[data-state]').map((el) => el.attributes('data-state'));
}

function label(wrapper) {
return wrapper.find('.pointer-events-none').text();
}

test('it fills the stops up to the selected percentage', () => {
const wrapper = mount(WidthSelector, { props: { modelValue: 50 } });

expect(states(wrapper)).toEqual(['selected', 'selected', 'selected', 'unselected', 'unselected', 'unselected']);
expect(label(wrapper)).toBe('50%');
});

test('it fills the stops for a width the stops do not offer', () => {
const wrapper = mount(WidthSelector, { props: { modelValue: 6 } });

expect(states(wrapper)).toEqual(['selected', 'selected', 'selected', 'unselected', 'unselected', 'unselected']);
expect(label(wrapper)).toBe('50%');
});

test('it fills every stop at full width', () => {
const wrapper = mount(WidthSelector, { props: { modelValue: 12 } });

expect(states(wrapper)).toEqual(Array(6).fill('selected'));
expect(label(wrapper)).toBe('100%');
});

test('it marks the last filled stop unless it is full width', () => {
expect(mount(WidthSelector, { props: { modelValue: 6 } }).findAll('[data-last="true"]')).toHaveLength(1);
expect(mount(WidthSelector, { props: { modelValue: 12 } }).findAll('[data-last="true"]')).toHaveLength(0);
});

test('it emits the raw stop value when clicked', async () => {
const wrapper = mount(WidthSelector, { props: { modelValue: 6 } });

await wrapper.findAll('[data-state]')[2].trigger('click');

expect(wrapper.emitted('update:model-value')).toEqual([[50]]);
});
72 changes: 72 additions & 0 deletions resources/js/tests/width.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { describe, it, expect } from 'vitest';
import { widthToColumnSpan, widthToPercentage } from '../util/width.js';

describe('widthToColumnSpan', () => {
it('passes spans through', () => {
for (let span = 1; span <= 12; span++) {
expect(widthToColumnSpan(span)).toBe(span);
}
});

it('resolves the percentages that shipped before spans', () => {
expect(widthToColumnSpan(25)).toBe(3);
expect(widthToColumnSpan(33)).toBe(4);
expect(widthToColumnSpan(50)).toBe(6);
expect(widthToColumnSpan(66)).toBe(8);
expect(widthToColumnSpan(75)).toBe(9);
expect(widthToColumnSpan(100)).toBe(12);
});

it('does not resolve the widgets-only size keywords', () => {
expect(widthToColumnSpan('sm')).toBe(12);
expect(widthToColumnSpan('md')).toBe(12);
expect(widthToColumnSpan('lg')).toBe(12);
expect(widthToColumnSpan('full')).toBe(12);
});

it('treats numbers above twelve as percentages', () => {
expect(widthToColumnSpan(13)).toBe(2);
expect(widthToColumnSpan(40)).toBe(5);
expect(widthToColumnSpan(90)).toBe(11);
});

it('clamps percentages beyond a full row', () => {
expect(widthToColumnSpan(200)).toBe(12);
});

it('handles numeric strings', () => {
expect(widthToColumnSpan('50')).toBe(6);
expect(widthToColumnSpan('6')).toBe(6);
});

it('falls back to full width for anything it does not recognise', () => {
expect(widthToColumnSpan(undefined)).toBe(12);
expect(widthToColumnSpan(null)).toBe(12);
expect(widthToColumnSpan('')).toBe(12);
expect(widthToColumnSpan('huge')).toBe(12);
expect(widthToColumnSpan(0)).toBe(12);
expect(widthToColumnSpan(-5)).toBe(12);
expect(widthToColumnSpan(7.5)).toBe(12);
});
});

describe('widthToPercentage', () => {
it('leaves percentages alone', () => {
expect(widthToPercentage(25)).toBe(25);
expect(widthToPercentage(33)).toBe(33);
expect(widthToPercentage(66)).toBe(66);
expect(widthToPercentage(100)).toBe(100);
});

it('derives a percentage from a span', () => {
expect(widthToPercentage(3)).toBe(25);
expect(widthToPercentage(4)).toBe(33);
expect(widthToPercentage(6)).toBe(50);
expect(widthToPercentage(9)).toBe(75);
expect(widthToPercentage(12)).toBe(100);
});

it('reports an unrecognised width as full width', () => {
expect(widthToPercentage(undefined)).toBe(100);
});
});
Loading
Loading