+`;
+
+export const _Sizes: Story = {
+ tags: ['!dev'],
+ parameters: {
+ docs: {
+ source: { code: sizesCode }
+ }
+ },
+ render: () => ({
+ components: { Field, StarRating },
+ setup() {
+ const small = ref(4);
+ const base = ref(4);
+ const large = ref(4);
+ return { small, base, large };
+ },
+ template: sizesCode,
+ }),
+};
+
+const disabledCode = `
+
+`;
+
+export const _Disabled: Story = {
+ tags: ['!dev'],
+ parameters: {
+ docs: {
+ source: { code: disabledCode }
+ }
+ },
+ render: () => ({
+ components: { StarRating },
+ template: disabledCode,
+ }),
+};
+
+export const TestStartsUnrated: Story = {
+ tags: ['!dev', 'test'],
+ render: () => ({
+ components: { StarRating },
+ template: ``,
+ }),
+ play: async ({ canvasElement }) => {
+ const input = within(canvasElement).getByRole('slider') as HTMLInputElement;
+
+ // The thumb sits at the minimum so it lines up with the first star, but nothing is filled in yet.
+ expect(input).toHaveAttribute('data-unrated');
+ expect(input.value).toBe('1');
+ },
+};
+
+export const TestArrowKeySelectsMinimum: Story = {
+ tags: ['!dev', 'test'],
+ args: {
+ 'onUpdate:modelValue': fn(),
+ },
+ render: (args) => ({
+ components: { StarRating },
+ setup() {
+ return { onUpdate: args['onUpdate:modelValue'] };
+ },
+ template: ``,
+ }),
+ play: async ({ canvasElement, args }) => {
+ const input = within(canvasElement).getByRole('slider') as HTMLInputElement;
+
+ input.focus();
+ await userEvent.keyboard('{ArrowRight}');
+
+ // Without intervention the browser would jump straight to two stars.
+ expect(args['onUpdate:modelValue']).toHaveBeenCalledWith(1);
+ expect(input).not.toHaveAttribute('data-unrated');
+ },
+};
+
+export const TestPointerRevealsTheFill: Story = {
+ tags: ['!dev', 'test'],
+ render: () => ({
+ components: { StarRating },
+ template: ``,
+ }),
+ play: async ({ canvasElement }) => {
+ const input = within(canvasElement).getByRole('slider') as HTMLInputElement;
+
+ await userEvent.click(input);
+
+ expect(input).not.toHaveAttribute('data-unrated');
+ },
+};
+
+export const TestHalfStars: Story = {
+ tags: ['!dev', 'test'],
+ render: () => ({
+ components: { StarRating },
+ template: ``,
+ }),
+ play: async ({ canvasElement }) => {
+ const input = within(canvasElement).getByRole('slider') as HTMLInputElement;
+
+ // The minimum falls back to a single step, so half stars can start at 0.5.
+ expect(input.min).toBe('0.5');
+ expect(input.max).toBe('10');
+ expect(input.step).toBe('0.5');
+ expect(input.value).toBe('3.5');
+ expect(input).not.toHaveAttribute('data-unrated');
+ },
+};
diff --git a/resources/js/stories/docs/ChoiceGrid.mdx b/resources/js/stories/docs/ChoiceGrid.mdx
new file mode 100644
index 00000000000..928a428731c
--- /dev/null
+++ b/resources/js/stories/docs/ChoiceGrid.mdx
@@ -0,0 +1,29 @@
+import { Canvas, Meta, ArgTypes } from '@storybook/addon-docs/blocks';
+import * as ChoiceGridStories from '../ChoiceGrid.stories';
+
+
+
+# ChoiceGrid
+A grid of image-led cards for picking between visual options. Each card is a real radio or checkbox, so selection works with the keyboard and inside a regular form.
+
+Options are objects with a `value`, and optionally a `label`, `image` and `badge`.
+
+
+## Multiple
+Add `multiple` to allow more than one selection. The value becomes an array of the selected option values.
+
+
+## Badges
+An option's `badge` is shown alongside its label. Handy for lettering or numbering the choices.
+
+
+## Columns, gaps and aspect ratios
+Use `columns` to set how many options appear per row, `gap` to control the spacing between them, and `aspect-ratio` to change the shape of each image area.
+
+
+## Missing images
+Options without an `image` fall back to a placeholder, so a partially configured grid still lines up.
+
+
+## Arguments
+
diff --git a/resources/js/stories/docs/RankList.mdx b/resources/js/stories/docs/RankList.mdx
new file mode 100644
index 00000000000..fb868d47ccf
--- /dev/null
+++ b/resources/js/stories/docs/RankList.mdx
@@ -0,0 +1,21 @@
+import { Canvas, Meta, ArgTypes } from '@storybook/addon-docs/blocks';
+import * as RankListStories from '../RankList.stories';
+
+
+
+# RankList
+An ordered list of items that can be rearranged by dragging, or by typing a position into each item's rank input.
+
+The value is an array of option values in their ranked order. When it's empty, or missing some of the options, the list falls back to the order the options were given in.
+
+
+## Existing order
+Pass an order to start from. The drag handles highlight once the list differs from the original order.
+
+
+## Disabled
+A disabled list can't be reordered. The rank inputs are replaced with plain numbers.
+
+
+## Arguments
+
diff --git a/resources/js/stories/docs/StarRating.mdx b/resources/js/stories/docs/StarRating.mdx
new file mode 100644
index 00000000000..78e2f278328
--- /dev/null
+++ b/resources/js/stories/docs/StarRating.mdx
@@ -0,0 +1,27 @@
+import { Canvas, Meta, ArgTypes } from '@storybook/addon-docs/blocks';
+import * as StarRatingStories from '../StarRating.stories';
+
+
+
+# StarRating
+A row of stars for collecting a rating. It's a native range input under the hood, so it's keyboard accessible.
+
+Nothing is filled in until the rating is set. The thumb still sits at the minimum so it lines up with the first star, but the fill stays hidden until the field is used or a value is provided.
+
+
+## Half stars
+Set `step` to `0.5` to allow ratings like 3.5.
+
+
+## Number of stars
+Use `max` to change how many stars are shown.
+
+
+## Sizes
+
+
+## Disabled
+
+
+## Arguments
+
diff --git a/resources/js/tests/Package.test.js b/resources/js/tests/Package.test.js
index 4da3ead2380..5d95c36e2ca 100644
--- a/resources/js/tests/Package.test.js
+++ b/resources/js/tests/Package.test.js
@@ -135,6 +135,7 @@ it('exports ui', async () => {
'CharacterCounter',
'Checkbox',
'CheckboxGroup',
+ 'ChoiceGrid',
'CodeEditor',
'Combobox',
'ConfirmationModal',
@@ -184,6 +185,7 @@ it('exports ui', async () => {
'Popover',
'Radio',
'RadioGroup',
+ 'RankList',
'Select',
'Separator',
'Skeleton',
@@ -191,6 +193,7 @@ it('exports ui', async () => {
'SplitterGroup',
'SplitterPanel',
'SplitterResizeHandle',
+ 'StarRating',
'Subheading',
'Switch',
'TabContent',