srcdev-nuxt-components 9.4.6 → 9.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/.claude/commands/migrate-component.md +34 -3
  2. package/.claude/component-ledger/audit.json +1 -1
  3. package/.claude/component-ledger/build.mjs +27 -0
  4. package/.claude/component-ledger/output.html +8 -3
  5. package/.claude/component-ledger/template.html +7 -2
  6. package/.claude/skills/components/column-flow-grid.md +93 -0
  7. package/.claude/skills/components/entry-animation.md +88 -0
  8. package/.claude/skills/components/input-copy.md +2 -0
  9. package/.claude/skills/components/input-text-core.md +186 -0
  10. package/.claude/skills/components/masonry-grid.md +153 -0
  11. package/.claude/skills/components/pop-over.md +100 -0
  12. package/.claude/skills/index.md +6 -1
  13. package/.vscode/srcdev-component-column-flow-grid.code-snippets +39 -0
  14. package/.vscode/srcdev-component-entry-animation.code-snippets +29 -0
  15. package/.vscode/srcdev-component-input-text.code-snippets +107 -0
  16. package/.vscode/srcdev-component-masonry-grid.code-snippets +51 -0
  17. package/.vscode/srcdev-component-pop-over.code-snippets +44 -0
  18. package/app/components/01.atoms/animations/entry/EntryAnimation.vue +7 -1
  19. package/app/components/01.atoms/animations/entry/stories/EntryAnimation.stories.ts +47 -0
  20. package/app/components/01.atoms/animations/entry/tests/EntryAnimation.spec.ts +57 -0
  21. package/app/components/01.atoms/canvas-switcher/stories/CanvasSwitcher.stories.ts +9 -10
  22. package/app/components/01.atoms/content-wrappers/docs-pages/stories/ContentDocs.stories.ts +12 -13
  23. package/app/components/01.atoms/grids/column-flow-grid/CONSUMER-STYLING.md +33 -0
  24. package/app/components/01.atoms/grids/column-flow-grid/ColumnFlowGrid.vue +55 -0
  25. package/app/components/01.atoms/grids/column-flow-grid/stories/ColumnFlowGrid.stories.ts +178 -0
  26. package/app/components/01.atoms/grids/column-flow-grid/tests/ColumnFlowGrid.spec.ts +75 -0
  27. package/app/components/01.atoms/grids/masonry-grid/CONSUMER-STYLING.md +35 -0
  28. package/app/components/01.atoms/grids/masonry-grid/MasonryGrid.vue +178 -0
  29. package/app/components/01.atoms/grids/masonry-grid/stories/MasonryGrid.stories.ts +199 -0
  30. package/app/components/01.atoms/grids/masonry-grid/tests/MasonryGrid.spec.ts +158 -0
  31. package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +54 -0
  32. package/app/components/01.atoms/pop-over/PopOver.vue +201 -0
  33. package/app/components/01.atoms/pop-over/stories/PopOver.stories.ts +141 -0
  34. package/app/components/01.atoms/pop-over/tests/PopOver.spec.ts +195 -0
  35. package/app/components/01.atoms/pop-over/tests/__snapshots__/PopOver.spec.ts.snap +11 -0
  36. package/app/components/02.molecules/action-menu/stories/ActionMenu.stories.ts +8 -2
  37. package/app/components/02.molecules/input-copy/InputCopy.vue +14 -0
  38. package/app/components/02.molecules/input-copy/stories/InputCopy.stories.ts +15 -0
  39. package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +41 -0
  40. package/app/components/05.forms/input-select/InputSelectCore.vue +1 -1
  41. package/app/components/05.forms/input-text/InputTextCore.vue +6 -0
  42. package/app/components/05.forms/input-text/stories/InputPasswordWithLabel.stories.ts +49 -20
  43. package/app/components/05.forms/input-text/stories/InputTextAsNumberWithLabel.stories.ts +41 -22
  44. package/app/components/05.forms/input-text/stories/InputTextCore.stories.ts +48 -22
  45. package/app/components/05.forms/input-text/stories/InputTextWithLabel.stories.ts +43 -17
  46. package/app/components/05.forms/input-text/tests/InputPasswordWithLabel.spec.ts +56 -0
  47. package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +61 -0
  48. package/app/components/05.forms/input-text/tests/InputTextCore.spec.ts +60 -0
  49. package/app/components/05.forms/input-text/tests/InputTextWithLabel.spec.ts +46 -0
  50. package/app/components/05.forms/input-text/variants/InputPasswordWithLabel.vue +7 -1
  51. package/app/components/05.forms/input-text/variants/InputTextAsNumberWithLabel.vue +8 -2
  52. package/app/components/05.forms/input-text/variants/InputTextWithLabel.vue +6 -0
  53. package/app/components/05.forms/input-textarea/stories/InputTextareaCore.stories.ts +20 -20
  54. package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +32 -26
  55. package/app/types/forms/types.forms.d.ts +1 -1
  56. package/package.json +1 -1
  57. package/app/components/masonry-grid/MasonryGrid.vue +0 -68
  58. package/app/components/masonry-grid-ordered/MasonryGridOrdered.vue +0 -163
  59. package/app/components/masonry-grid-ordered/MasonryGridOrderedGridExperiment.vue +0 -259
  60. package/app/components/masonry-grid-ordered/stories/MasonryGridOrdered.stories.ts +0 -354
  61. package/app/components/masonry-grid-sorted/MasonryGridSorted.vue +0 -120
  62. package/app/components/pop-over/PopOver.vue +0 -90
  63. package/app/layouts/default.vue +0 -308
  64. package/app/layouts/site-navigation-demo.vue +0 -188
@@ -0,0 +1,39 @@
1
+ {
2
+ "SRCDEV ColumnFlowGrid Basic": {
3
+ "description": "Basic ColumnFlowGrid with a few named item slots",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<ColumnFlowGrid :item-min-width=\"$1300\" :gap=\"$21.2\">",
7
+ " <template #item-1>",
8
+ " $3",
9
+ " </template>",
10
+ " <template #item-2>",
11
+ " $4",
12
+ " </template>",
13
+ "</ColumnFlowGrid>"
14
+ ]
15
+ },
16
+ "SRCDEV ColumnFlowGrid Looped": {
17
+ "description": "ColumnFlowGrid rendering a v-for list via dynamic slot names (no count prop needed)",
18
+ "scope": "vue,html",
19
+ "body": [
20
+ "<ColumnFlowGrid :item-min-width=\"$1280\" :gap=\"$21.6\">",
21
+ " <template v-for=\"($3item, $4index) in $5items\" :key=\"$3item.id\" #[`item-${$4index}`]>",
22
+ " $6",
23
+ " </template>",
24
+ "</ColumnFlowGrid>"
25
+ ]
26
+ },
27
+ "SRCDEV ColumnFlowGrid CSS Override": {
28
+ "description": "CSS override scaffold for ColumnFlowGrid tokens",
29
+ "scope": "css",
30
+ "body": [
31
+ ".$1my-page {",
32
+ " .column-flow-grid-wrapper {",
33
+ " --column-flow-grid-item-border-colour: $2;",
34
+ " --column-flow-grid-item-padding: $3;",
35
+ " }",
36
+ "}"
37
+ ]
38
+ }
39
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "SRCDEV EntryAnimation Basic": {
3
+ "description": "Basic EntryAnimation wrapper with a default slide-in animation",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<EntryAnimation animation-type=\"$1entry-slide-in\">",
7
+ " <template #default>",
8
+ " $2",
9
+ " </template>",
10
+ "</EntryAnimation>"
11
+ ]
12
+ },
13
+ "SRCDEV EntryAnimation Looped, Skip First": {
14
+ "description": "EntryAnimation in a v-for loop, skipping the first (above-the-fold) item",
15
+ "scope": "vue,html",
16
+ "body": [
17
+ "<EntryAnimation",
18
+ " v-for=\"($1item, $2index) in $3items\"",
19
+ " :key=\"$1item.id\"",
20
+ " :skip-animation=\"$2index === 0\"",
21
+ " animation-type=\"$4entry-slide-in\"",
22
+ ">",
23
+ " <template #default>",
24
+ " $5",
25
+ " </template>",
26
+ "</EntryAnimation>"
27
+ ]
28
+ }
29
+ }
@@ -0,0 +1,107 @@
1
+ {
2
+ "SRCDEV InputTextCore Basic": {
3
+ "description": "Basic InputTextCore native input (no label/error wrapper)",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<InputTextCore",
7
+ " id=\"$1fieldId\"",
8
+ " name=\"$2fieldName\"",
9
+ " v-model=\"$3fieldValue\"",
10
+ " type=\"$4text\"",
11
+ " placeholder=\"$5Enter value...\"",
12
+ "/>"
13
+ ]
14
+ },
15
+ "SRCDEV InputTextCore Date with min/max": {
16
+ "description": "InputTextCore date input constrained to a min/max range (e.g. no dates in the past)",
17
+ "scope": "vue,html",
18
+ "body": [
19
+ "<InputTextCore",
20
+ " id=\"$1appointment-date\"",
21
+ " name=\"$2appointmentDate\"",
22
+ " v-model=\"$3appointmentDate\"",
23
+ " type=\"date\"",
24
+ " :min=\"new Date().toISOString().split('T')[0]\"",
25
+ " max=\"$42027-12-31\"",
26
+ "/>"
27
+ ]
28
+ },
29
+ "SRCDEV InputTextWithLabel Basic": {
30
+ "description": "InputTextWithLabel: labelled text field with error/description support",
31
+ "scope": "vue,html",
32
+ "body": [
33
+ "<InputTextWithLabel",
34
+ " v-model=\"$1fieldValue\"",
35
+ " id=\"$2fieldId\"",
36
+ " name=\"$3fieldName\"",
37
+ " type=\"$4text\"",
38
+ " label=\"$5Field label\"",
39
+ " :error-message=\"$6errors.fieldName\"",
40
+ " :field-has-error=\"!!$6errors.fieldName\"",
41
+ "/>"
42
+ ]
43
+ },
44
+ "SRCDEV InputTextWithLabel Date with min/max": {
45
+ "description": "InputTextWithLabel date field constrained to a min/max range",
46
+ "scope": "vue,html",
47
+ "body": [
48
+ "<InputTextWithLabel",
49
+ " v-model=\"$1appointmentDate\"",
50
+ " id=\"$2appointment-date\"",
51
+ " name=\"$2appointmentDate\"",
52
+ " type=\"date\"",
53
+ " label=\"$3Appointment date\"",
54
+ " :min=\"new Date().toISOString().split('T')[0]\"",
55
+ " max=\"$42027-12-31\"",
56
+ " :error-message=\"$5errors.appointmentDate\"",
57
+ " :field-has-error=\"!!$5errors.appointmentDate\"",
58
+ "/>"
59
+ ]
60
+ },
61
+ "SRCDEV InputPasswordWithLabel": {
62
+ "description": "InputPasswordWithLabel with show/hide toggle",
63
+ "scope": "vue,html",
64
+ "body": [
65
+ "<InputPasswordWithLabel",
66
+ " v-model=\"$1password\"",
67
+ " name=\"$2password\"",
68
+ " label=\"$3Password\"",
69
+ " :error-message=\"$4errors.password\"",
70
+ " :field-has-error=\"!!$4errors.password\"",
71
+ "/>"
72
+ ]
73
+ },
74
+ "SRCDEV InputTextAsNumberWithLabel": {
75
+ "description": "InputTextAsNumberWithLabel numeric stepper with min/max/step",
76
+ "scope": "vue,html",
77
+ "body": [
78
+ "<InputTextAsNumberWithLabel",
79
+ " v-model=\"$1quantity\"",
80
+ " name=\"$2quantity\"",
81
+ " label=\"$3Quantity\"",
82
+ " :min=\"$41\"",
83
+ " :max=\"$599\"",
84
+ " :step=\"$61\"",
85
+ " :error-message=\"$7errors.quantity\"",
86
+ " :field-has-error=\"!!$7errors.quantity\"",
87
+ ">",
88
+ " <template #left>−</template>",
89
+ " <template #right>+</template>",
90
+ "</InputTextAsNumberWithLabel>"
91
+ ]
92
+ },
93
+ "SRCDEV InputText CSS Override": {
94
+ "description": "CSS override scaffold for InputTextCore tokens",
95
+ "scope": "css",
96
+ "body": [
97
+ ".$1my-page {",
98
+ " .input-text-wrapper {",
99
+ " --input-text-surface: $2;",
100
+ " --input-text-border: $3;",
101
+ " --input-text-border-focus: $4;",
102
+ " --input-text-color: $5;",
103
+ " }",
104
+ "}"
105
+ ]
106
+ }
107
+ }
@@ -0,0 +1,51 @@
1
+ {
2
+ "SRCDEV MasonryGrid Basic": {
3
+ "description": "Basic MasonryGrid with a few named item slots",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<MasonryGrid :item-min-width=\"$1300\" :gap=\"$212\">",
7
+ " <template #item-1>",
8
+ " $3",
9
+ " </template>",
10
+ " <template #item-2>",
11
+ " $4",
12
+ " </template>",
13
+ "</MasonryGrid>"
14
+ ]
15
+ },
16
+ "SRCDEV MasonryGrid Looped": {
17
+ "description": "MasonryGrid rendering a v-for list via dynamic slot names (no count prop needed)",
18
+ "scope": "vue,html",
19
+ "body": [
20
+ "<MasonryGrid :item-min-width=\"$1280\" :gap=\"$216\">",
21
+ " <template v-for=\"($3item, $4index) in $5items\" :key=\"$3item.id\" #[`item-${$4index}`]>",
22
+ " $6",
23
+ " </template>",
24
+ "</MasonryGrid>"
25
+ ]
26
+ },
27
+ "SRCDEV MasonryGrid Fixed Width Centered": {
28
+ "description": "MasonryGrid with fixed-width tiles centered in the wrapper",
29
+ "scope": "vue,html",
30
+ "body": [
31
+ "<MasonryGrid :fixed-width=\"true\" justify=\"center\" :item-min-width=\"$1240\" :gap=\"$212\">",
32
+ " <template v-for=\"($3item, $4index) in $5items\" :key=\"$3item.id\" #[`item-${$4index}`]>",
33
+ " $6",
34
+ " </template>",
35
+ "</MasonryGrid>"
36
+ ]
37
+ },
38
+ "SRCDEV MasonryGrid CSS Override": {
39
+ "description": "CSS override scaffold for MasonryGrid tokens",
40
+ "scope": "css",
41
+ "body": [
42
+ ".$1my-page {",
43
+ " .masonry-grid-wrapper {",
44
+ " --masonry-grid-item-border-colour: $2;",
45
+ " --masonry-grid-item-padding: $3;",
46
+ " --masonry-grid-transition-duration: $4;",
47
+ " }",
48
+ "}"
49
+ ]
50
+ }
51
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "SRCDEV PopOver Basic": {
3
+ "description": "PopOver — generic anchor-positioned disclosure panel",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<PopOver>",
7
+ " <template #trigger>",
8
+ " <span>$1</span>",
9
+ " </template>",
10
+ " <template #content>",
11
+ " <p>$0</p>",
12
+ " </template>",
13
+ "</PopOver>"
14
+ ]
15
+ },
16
+ "SRCDEV PopOver Placement": {
17
+ "description": "PopOver with an explicit placement",
18
+ "scope": "vue,html",
19
+ "body": [
20
+ "<PopOver placement=\"${1|top,right,bottom,left|}\">",
21
+ " <template #trigger>",
22
+ " <span>$2</span>",
23
+ " </template>",
24
+ " <template #content>",
25
+ " <p>$0</p>",
26
+ " </template>",
27
+ "</PopOver>"
28
+ ]
29
+ },
30
+ "SRCDEV PopOver Icon Trigger": {
31
+ "description": "PopOver with an icon-only trigger and localised labels",
32
+ "scope": "vue,html",
33
+ "body": [
34
+ "<PopOver trigger-aria-label=\"${1:Show more}\" popover-aria-label=\"${2:More information}\">",
35
+ " <template #trigger>",
36
+ " <Icon name=\"${3:lucide:info}\" aria-hidden=\"true\" />",
37
+ " </template>",
38
+ " <template #content>",
39
+ " <p>$0</p>",
40
+ " </template>",
41
+ "</PopOver>"
42
+ ]
43
+ }
44
+ }
@@ -8,15 +8,21 @@
8
8
  interface Props {
9
9
  tag?: "div" | "section" | "article" | "aside";
10
10
  animationType?: "entry-slide-in" | "entry-zoom-reveal" | "entry-exit-blur";
11
+ /** Renders as a plain element with no animation class — e.g. the first item in a looped list
12
+ * that's already above the fold, where an entry animation would just delay visible content. */
13
+ skipAnimation?: boolean;
11
14
  styleClassPassthrough?: string | string[];
12
15
  }
13
16
  const props = withDefaults(defineProps<Props>(), {
14
17
  tag: "div",
15
18
  animationType: "entry-slide-in",
19
+ skipAnimation: false,
16
20
  styleClassPassthrough: () => [],
17
21
  });
18
22
 
19
23
  const { elementClasses, updateElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
20
24
 
21
- updateElementClasses(props.animationType);
25
+ if (!props.skipAnimation) {
26
+ updateElementClasses(props.animationType);
27
+ }
22
28
  </script>
@@ -17,6 +17,12 @@ const meta: Meta<typeof EntryAnimation> = {
17
17
  description: "Type of entry animation",
18
18
  defaultValue: "entry-slide-in",
19
19
  },
20
+ skipAnimation: {
21
+ control: { type: "boolean" },
22
+ description:
23
+ "Render as a plain element with no animation class — e.g. the first item in a looped list that's already above the fold",
24
+ defaultValue: false,
25
+ },
20
26
  styleClassPassthrough: {
21
27
  control: { type: "text" },
22
28
  description: "Additional classes for styling",
@@ -72,3 +78,44 @@ export const EntryExitBlur: StoryObj<typeof EntryAnimation> = {
72
78
  },
73
79
  render: EntrySlideIn.render,
74
80
  };
81
+
82
+ export const SkippedInALoop: StoryObj<typeof EntryAnimation> = {
83
+ name: "Skip first item (looped list, above the fold)",
84
+ parameters: {
85
+ docs: {
86
+ description: {
87
+ story:
88
+ "The common `v-for` case: the first item is already above the fold, so animating it in " +
89
+ "would just delay content the visitor can already see. Pass `skip-animation` for that one " +
90
+ "item instead of swapping the whole component out for a plain tag.",
91
+ },
92
+ },
93
+ },
94
+ render: () => ({
95
+ components: { EntryAnimation },
96
+ setup() {
97
+ const items = [0, 1, 2];
98
+ return { items };
99
+ },
100
+ template: `
101
+ <div style="padding-block: 400px; background-color: green; margin-bottom: 1000px;">
102
+ <label style="display: block; font-weight: bold; margin-bottom: 1rem; color: #fff;">
103
+ Item 0 renders immediately (skip-animation) — scroll to see items 1 and 2 animate in
104
+ </label>
105
+ <div
106
+ v-for="index in items"
107
+ :key="index"
108
+ style="margin-bottom: 2rem;"
109
+ >
110
+ <EntryAnimation :skip-animation="index === 0" animation-type="entry-slide-in">
111
+ <template #default>
112
+ <div style="padding: 2rem; background: #f0f0f0; border-radius: 8px;">
113
+ Item {{ index }} {{ index === 0 ? '(skipped)' : '(animated)' }}
114
+ </div>
115
+ </template>
116
+ </EntryAnimation>
117
+ </div>
118
+ </div>
119
+ `,
120
+ }),
121
+ };
@@ -0,0 +1,57 @@
1
+ // https://nuxt.com/docs/getting-started/testing#unit-testing
2
+ import type { VueWrapper } from "@vue/test-utils";
3
+ import { mountSuspended } from "@nuxt/test-utils/runtime";
4
+ import ComponentUnderTest from "../EntryAnimation.vue";
5
+
6
+ let wrapper: VueWrapper<InstanceType<typeof ComponentUnderTest>>;
7
+ const wrapperFactory = (propsData = {}, slotsData = {}) => {
8
+ return mountSuspended(ComponentUnderTest, {
9
+ attachTo: document.body,
10
+ props: propsData,
11
+ slots: {
12
+ default: () => "Content",
13
+ ...slotsData,
14
+ },
15
+ });
16
+ };
17
+
18
+ describe("EntryAnimation", () => {
19
+ it("mounts without error", async () => {
20
+ wrapper = await wrapperFactory();
21
+ expect(wrapper.vm).toBeTruthy();
22
+ });
23
+
24
+ it("renders the given tag", async () => {
25
+ wrapper = await wrapperFactory({ tag: "section" });
26
+ expect(wrapper.element.tagName).toBe("SECTION");
27
+ });
28
+
29
+ it("applies the default animation class", async () => {
30
+ wrapper = await wrapperFactory();
31
+ expect(wrapper.classes()).toContain("entry-slide-in");
32
+ });
33
+
34
+ it("applies the requested animationType class", async () => {
35
+ wrapper = await wrapperFactory({ animationType: "entry-zoom-reveal" });
36
+ expect(wrapper.classes()).toContain("entry-zoom-reveal");
37
+ expect(wrapper.classes()).not.toContain("entry-slide-in");
38
+ });
39
+
40
+ it("applies no animation class when skipAnimation is true", async () => {
41
+ wrapper = await wrapperFactory({ skipAnimation: true });
42
+ expect(wrapper.classes()).not.toContain("entry-slide-in");
43
+ expect(wrapper.classes()).not.toContain("entry-zoom-reveal");
44
+ expect(wrapper.classes()).not.toContain("entry-exit-blur");
45
+ });
46
+
47
+ it("still applies styleClassPassthrough classes when skipAnimation is true", async () => {
48
+ wrapper = await wrapperFactory({ skipAnimation: true, styleClassPassthrough: ["custom-class"] });
49
+ expect(wrapper.classes()).toContain("custom-class");
50
+ expect(wrapper.classes()).not.toContain("entry-slide-in");
51
+ });
52
+
53
+ it("renders slot content", async () => {
54
+ wrapper = await wrapperFactory();
55
+ expect(wrapper.text()).toContain("Content");
56
+ });
57
+ });
@@ -37,17 +37,18 @@ export default {
37
37
  },
38
38
  } as Meta<typeof CanvasSwitcher>;
39
39
 
40
+ // `args` is Storybook's own reactive object — bind to it directly (`args.x`) rather than
41
+ // destructuring it into local variables/refs, which would snapshot the values once at setup()
42
+ // and stop reflecting later Controls-panel changes.
40
43
  const Template: StoryFn<CanvasSwitcherStoryArgs> = (args) => ({
41
44
  components: { CanvasSwitcher },
42
45
  setup() {
43
- const { canvasName, ...otherArgs } = args;
44
- const selected = ref(canvasName);
45
- return { selected, args: otherArgs };
46
+ return { args };
46
47
  },
47
48
  template: `
48
49
  <div>
49
- <CanvasSwitcher v-model:canvas-name="selected" v-bind="args" />
50
- <p style="margin-top: 1.6rem;">Selected: {{ selected }}</p>
50
+ <CanvasSwitcher v-model:canvas-name="args.canvasName" v-bind="args" />
51
+ <p style="margin-top: 1.6rem;">Selected: {{ args.canvasName }}</p>
51
52
  </div>
52
53
  `,
53
54
  });
@@ -60,14 +61,12 @@ PreviewWidthDemo.args = {};
60
61
  PreviewWidthDemo.render = (args) => ({
61
62
  components: { CanvasSwitcher },
62
63
  setup() {
63
- const { canvasName } = args;
64
- const selected = ref(canvasName);
65
- return { selected };
64
+ return { args };
66
65
  },
67
66
  template: `
68
67
  <div>
69
- <CanvasSwitcher v-model:canvas-name="selected" />
70
- <div :class="selected" style="border: 1px dashed currentColor; margin-top: 1.6rem; padding: 1.6rem;">
68
+ <CanvasSwitcher v-model:canvas-name="args.canvasName" />
69
+ <div :class="args.canvasName" style="border: 1px dashed currentColor; margin-top: 1.6rem; padding: 1.6rem;">
71
70
  This box is constrained by the selected canvas's utility class.
72
71
  </div>
73
72
  </div>
@@ -1,4 +1,5 @@
1
1
  import type { Meta, StoryFn } from "@nuxtjs/storybook";
2
+ import { computed } from "vue";
2
3
  import ContentDocs from "../ContentDocs.vue";
3
4
  import type { DocsNavItem } from "~/types/components";
4
5
 
@@ -86,19 +87,19 @@ export default {
86
87
 
87
88
  // ─── Stories ─────────────────────────────────────────────────────────────────
88
89
 
90
+ // `args` is Storybook's own reactive object — bind to it directly (`args.x`) rather than
91
+ // destructuring it into local variables/refs, which would snapshot the values once at setup()
92
+ // and stop reflecting later Controls-panel changes.
89
93
  const Template: StoryFn<ContentDocsStoryArgs> = (args) => ({
90
94
  components: { ContentDocs },
91
95
  setup() {
92
- const { activeNavItem, activePageNavItem, ...rest } = args;
93
- const activeNav = ref(activeNavItem);
94
- const activePageNav = ref(activePageNavItem);
95
- return { args: rest, activeNav, activePageNav };
96
+ return { args };
96
97
  },
97
98
  template: `
98
99
  <ContentDocs
99
100
  v-bind="args"
100
- v-model:active-nav-item="activeNav"
101
- v-model:active-page-nav-item="activePageNav"
101
+ v-model:active-nav-item="args.activeNavItem"
102
+ v-model:active-page-nav-item="args.activePageNavItem"
102
103
  >
103
104
  <template #docsContent>
104
105
  <h3 style="margin-top:0">Docs Content</h3>
@@ -140,10 +141,8 @@ CustomLabels.args = {
140
141
  export const IconAtEnd: StoryFn<ContentDocsStoryArgs> = (args) => ({
141
142
  components: { ContentDocs },
142
143
  setup() {
143
- const { activeNavItem, activePageNavItem, ...rest } = args;
144
- const activeNav = ref(activeNavItem);
145
- const activePageNav = ref(activePageNavItem);
146
- return { args: { ...rest, styleClassPassthrough: ["icon-at-end-demo"] }, activeNav, activePageNav };
144
+ const componentArgs = computed(() => ({ ...args, styleClassPassthrough: ["icon-at-end-demo"] }));
145
+ return { args, componentArgs };
147
146
  },
148
147
  template: `
149
148
  <div>
@@ -154,9 +153,9 @@ export const IconAtEnd: StoryFn<ContentDocsStoryArgs> = (args) => ({
154
153
  }
155
154
  </style>
156
155
  <ContentDocs
157
- v-bind="args"
158
- v-model:active-nav-item="activeNav"
159
- v-model:active-page-nav-item="activePageNav"
156
+ v-bind="componentArgs"
157
+ v-model:active-nav-item="args.activeNavItem"
158
+ v-model:active-page-nav-item="args.activePageNavItem"
160
159
  >
161
160
  <template #docsContent>
162
161
  <h3 style="margin-top:0">Docs Content</h3>
@@ -0,0 +1,33 @@
1
+ # ColumnFlowGrid — Consumer Styling Guide
2
+
3
+ ## Public token API
4
+
5
+ | Token | Falls back to | Controls |
6
+ |---|---|---|
7
+ | `--column-flow-grid-item-border-colour` | `var(--theme-border)` | Outline colour around each item |
8
+ | `--column-flow-grid-item-padding` | `1.2rem` | Inner padding of each item |
9
+
10
+ Column width and gap are controlled via props (`item-min-width`, `gap`, `unit`), not CSS custom
11
+ properties — they drive the CSS column layout directly and need JS-computed values, so they aren't
12
+ part of the public token API. See `.claude/skills/components/column-flow-grid.md`.
13
+
14
+ ---
15
+
16
+ ## Global theming — app-level CSS file
17
+
18
+ ```css
19
+ :where(html) {
20
+ --column-flow-grid-item-border-colour: var(--rose-05);
21
+ --column-flow-grid-item-padding: 1.6rem;
22
+ }
23
+ ```
24
+
25
+ ---
26
+
27
+ ## Per-instance overrides
28
+
29
+ ```vue
30
+ <ColumnFlowGrid style="--column-flow-grid-item-border-colour: var(--gold-04);">
31
+ ...
32
+ </ColumnFlowGrid>
33
+ ```
@@ -0,0 +1,55 @@
1
+ <template>
2
+ <component
3
+ :is="tag"
4
+ class="column-flow-grid-wrapper"
5
+ :class="[elementClasses]"
6
+ :style="`--_column-flow-grid-gap: ${gap}${unit}; --_item-min-width: ${itemMinWidth}px`"
7
+ >
8
+ <div v-for="(_, name) in $slots" :key="name" class="column-flow-grid-item">
9
+ <slot :name="name"></slot>
10
+ </div>
11
+ </component>
12
+ </template>
13
+
14
+ <script setup lang="ts">
15
+ interface Props {
16
+ tag?: "div" | "section" | "article" | "main";
17
+ itemMinWidth?: number;
18
+ gap?: number;
19
+ unit?: string;
20
+ styleClassPassthrough?: string | string[];
21
+ }
22
+
23
+ const props = withDefaults(defineProps<Props>(), {
24
+ tag: "div",
25
+ itemMinWidth: 300,
26
+ gap: 1.2,
27
+ unit: "rem",
28
+ styleClassPassthrough: () => [],
29
+ });
30
+
31
+ const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
32
+
33
+ watch(
34
+ () => props.styleClassPassthrough,
35
+ () => {
36
+ resetElementClasses(props.styleClassPassthrough);
37
+ }
38
+ );
39
+ </script>
40
+
41
+ <style lang="css">
42
+ @layer components {
43
+ .column-flow-grid-wrapper {
44
+ columns: auto var(--_item-min-width);
45
+ column-gap: var(--_column-flow-grid-gap);
46
+
47
+ .column-flow-grid-item {
48
+ break-inside: avoid;
49
+ outline: 0.1rem solid var(--column-flow-grid-item-border-colour, var(--theme-border));
50
+ padding: var(--column-flow-grid-item-padding, 1.2rem);
51
+ margin-block-end: var(--_column-flow-grid-gap);
52
+ }
53
+ }
54
+ }
55
+ </style>