srcdev-nuxt-components 9.6.2 → 9.6.4

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 (58) hide show
  1. package/.claude/rules/consumer/consumer-overrides.md +17 -0
  2. package/.claude/rules/consumer/css-comment-slash.md +13 -0
  3. package/.claude/rules/consumer/css-native-nesting.md +13 -0
  4. package/.claude/rules/consumer/css-no-deep.md +13 -0
  5. package/.claude/rules/consumer/css-rem-base.md +11 -0
  6. package/.claude/rules/consumer/css-surface-text-pairing.md +13 -0
  7. package/.claude/rules/consumer/css-webkit-support.md +15 -0
  8. package/.claude/rules/consumer/explicit-vue-imports.md +11 -0
  9. package/.claude/rules/consumer/nuxt-img-dimensions.md +12 -0
  10. package/.claude/rules/consumer/srcdev-skills-location.md +9 -0
  11. package/.claude/rules/consumer/story-args.md +12 -0
  12. package/.claude/rules/consumer/story-register-components.md +12 -0
  13. package/.claude/rules/consumer/vue-auto-imports.md +14 -0
  14. package/.claude/rules/consumer/vue-no-self-closing.md +12 -0
  15. package/.claude/rules/consumer/vue-prop-hyphenation.md +11 -0
  16. package/.claude/rules/consumer/vue-props-pattern.md +13 -0
  17. package/.claude/rules/consumer/vue-slots-no-computed.md +14 -0
  18. package/.claude/rules/consumer/vue-video-source.md +12 -0
  19. package/.claude/skills/component-compliance-checklist.md +31 -3
  20. package/.claude/skills/component-inline-action-button.md +33 -2
  21. package/.claude/skills/component-naming.md +1 -1
  22. package/.claude/skills/components/cookie-consent-banner.md +9 -6
  23. package/.claude/skills/components/input-copy.md +47 -100
  24. package/.claude/skills/components/pricing-card.md +68 -84
  25. package/.claude/skills/composable-cookie-consent.md +1 -1
  26. package/.claude/skills/index.md +11 -5
  27. package/.claude/skills/new-app-scaffold.md +4 -2
  28. package/.claude/skills/setup-postinstall.md +5 -4
  29. package/.claude/skills/storybook-add-story.md +44 -0
  30. package/.claude/skills/testing-add-unit-test.md +19 -0
  31. package/.claude/skills/theming-component-token-pattern.md +47 -1
  32. package/.claude/skills/theming-partial-override.md +1 -1
  33. package/.vscode/srcdev-component-cookie-consent-banner.code-snippets +7 -5
  34. package/.vscode/srcdev-component-input-copy.code-snippets +24 -42
  35. package/.vscode/srcdev-component-pricing-card.code-snippets +46 -84
  36. package/README.md +5 -5
  37. package/app/components/01.atoms/animations/scroll-reveal-frame/stories/ScrollRevealFrame.stories.ts +2 -0
  38. package/app/components/01.atoms/animations/scroll-reveal-image/stories/ScrollRevealImage.stories.ts +2 -0
  39. package/app/components/01.atoms/animations/section-parallax/stories/SectionParallax.stories.ts +4 -0
  40. package/app/components/01.atoms/animations/view-timeline/stories/WipeAwayVertical.stories.ts +2 -0
  41. package/app/components/02.molecules/cookie-consent-banner/CONSUMER-STYLING.md +13 -4
  42. package/app/components/02.molecules/cookie-consent-banner/CookieConsentBanner.vue +23 -14
  43. package/app/components/02.molecules/cookie-consent-banner/stories/CookieConsentBanner.stories.ts +108 -5
  44. package/app/components/02.molecules/cookie-consent-banner/tests/CookieConsentBanner.spec.ts +33 -11
  45. package/app/components/02.molecules/input-copy/CONSUMER-STYLING.md +82 -124
  46. package/app/components/02.molecules/input-copy/InputCopy.vue +110 -85
  47. package/app/components/02.molecules/input-copy/stories/InputCopy.stories.ts +204 -48
  48. package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +147 -171
  49. package/app/components/02.molecules/pricing-card/CONSUMER-STYLING.md +96 -149
  50. package/app/components/02.molecules/pricing-card/PricingCard.vue +84 -44
  51. package/app/components/02.molecules/pricing-card/stories/PricingCard.stories.ts +241 -104
  52. package/app/components/02.molecules/pricing-card/tests/PricingCard.spec.ts +149 -123
  53. package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +4 -0
  54. package/app/components/02.molecules/toast/stories/DisplayToastProvider.stories.ts +4 -0
  55. package/app/components/05.forms/toggle-switch/ToggleSwitchWithLabel.vue +1 -1
  56. package/app/composables/useAriaDescribedById.ts +1 -1
  57. package/app/types/components/cookie-consent-banner.d.ts +2 -0
  58. package/package.json +2 -1
@@ -34,6 +34,12 @@ function rejectButton() {
34
34
  return document.querySelector("[data-test-id='cookie-consent-banner-reject']") as HTMLElement;
35
35
  }
36
36
 
37
+ // Vue ignores events stamped at or before its listener's attach time; fake timers freeze both.
38
+ function click(button: HTMLElement) {
39
+ vi.advanceTimersByTime(1);
40
+ button.click();
41
+ }
42
+
37
43
  describe("CookieConsentBanner", () => {
38
44
  beforeEach(() => {
39
45
  statusRef.value = "unset";
@@ -56,15 +62,15 @@ describe("CookieConsentBanner", () => {
56
62
  expect(banner()).not.toBeNull();
57
63
  });
58
64
 
59
- it("is visible (no closed class) while status is 'unset'", async () => {
65
+ it("is visible (no closed state class) while status is 'unset'", async () => {
60
66
  await mountSuspended(CookieConsentBanner);
61
- expect(banner()!.classList).not.toContain("closed");
67
+ expect(banner()!.classList).not.toContain("cookie-consent-banner-closed");
62
68
  });
63
69
 
64
- it("adds the closed class once status is no longer 'unset'", async () => {
70
+ it("adds the cookie-consent-banner-closed class once status is no longer 'unset'", async () => {
65
71
  statusRef.value = "granted";
66
72
  await mountSuspended(CookieConsentBanner);
67
- expect(banner()!.classList).toContain("closed");
73
+ expect(banner()!.classList).toContain("cookie-consent-banner-closed");
68
74
  });
69
75
 
70
76
  it("defaults to data-theme='info'", async () => {
@@ -94,21 +100,37 @@ describe("CookieConsentBanner", () => {
94
100
  expect(banner()!.textContent).toContain("Custom cookie copy");
95
101
  });
96
102
 
97
- it("renders acceptLabel/rejectLabel slot content", async () => {
98
- await mountSuspended(CookieConsentBanner, { slots: { acceptLabel: "Yes please", rejectLabel: "No thanks" } });
103
+ it("renders default button labels", async () => {
104
+ await mountSuspended(CookieConsentBanner);
105
+ expect(acceptButton().textContent).toContain("Accept");
106
+ expect(rejectButton().textContent).toContain("Reject");
107
+ });
108
+
109
+ it("renders acceptLabel/rejectLabel props", async () => {
110
+ await mountSuspended(CookieConsentBanner, { props: { acceptLabel: "Yes please", rejectLabel: "No thanks" } });
99
111
  expect(acceptButton().textContent).toContain("Yes please");
100
112
  expect(rejectButton().textContent).toContain("No thanks");
101
113
  });
102
114
 
115
+ it("renders both buttons as inline InputButtons", async () => {
116
+ await mountSuspended(CookieConsentBanner);
117
+ for (const button of [acceptButton(), rejectButton()]) {
118
+ expect(button.tagName).toBe("BUTTON");
119
+ expect(button.getAttribute("type")).toBe("button");
120
+ expect(button.classList).toContain("input-button");
121
+ expect(button.classList).toContain("inline");
122
+ }
123
+ });
124
+
103
125
  it("calls acceptAll() when the accept button is clicked", async () => {
104
126
  await mountSuspended(CookieConsentBanner);
105
- acceptButton().click();
127
+ click(acceptButton());
106
128
  expect(acceptAllSpy).toHaveBeenCalledOnce();
107
129
  });
108
130
 
109
131
  it("calls rejectAll() when the reject button is clicked", async () => {
110
132
  await mountSuspended(CookieConsentBanner);
111
- rejectButton().click();
133
+ click(rejectButton());
112
134
  expect(rejectAllSpy).toHaveBeenCalledOnce();
113
135
  });
114
136
 
@@ -133,9 +155,9 @@ describe("CookieConsentBanner", () => {
133
155
 
134
156
  it("closes after a choice is made", async () => {
135
157
  await mountSuspended(CookieConsentBanner);
136
- rejectButton().click();
158
+ click(rejectButton());
137
159
  await nextTick();
138
- expect(banner()!.classList).toContain("closed");
160
+ expect(banner()!.classList).toContain("cookie-consent-banner-closed");
139
161
  });
140
162
 
141
163
  it("reopens if consent is reset to 'unset'", async () => {
@@ -143,7 +165,7 @@ describe("CookieConsentBanner", () => {
143
165
  await mountSuspended(CookieConsentBanner);
144
166
  statusRef.value = "unset";
145
167
  await nextTick();
146
- expect(banner()!.classList).not.toContain("closed");
168
+ expect(banner()!.classList).not.toContain("cookie-consent-banner-closed");
147
169
  });
148
170
 
149
171
  it("updates classes when styleClassPassthrough changes", async () => {
@@ -1,150 +1,108 @@
1
1
  # InputCopy — Consumer Styling Guide
2
2
 
3
+ A readonly input and a copy button (an `InputButton` with `variant="inline"`, so the tokens below
4
+ style it rather than `InputButton`'s variant colours), with optional description text below.
5
+
6
+ > **Changed 2026-10-15**: compliance migration.
7
+ >
8
+ > - Colour defaults now come from the theme: input background `--theme-input-surface` (was
9
+ > `--slate-00`), border `0.1rem solid var(--theme-border)` (was `1px solid var(--slate-03)`), input
10
+ > and description text `--theme-text` (were `#333` / `#666`). Set the tokens if you relied on the
11
+ > old greys.
12
+ > - Font sizes were `0.95rem` / `0.85rem` (9.5px / 8.5px on the 10px rem base); both now default to
13
+ > `var(--step-4)`.
14
+ > - `--input-copy-description-margin` defaults to `0` (was `0.6rem 0 0 0`, doubling up with the
15
+ > gap); spacing is the new `--input-copy-gap`.
16
+ > - `--input-copy-input-placeholder-color` is gone (the input always has a value). New:
17
+ > `--input-copy-gap`, `--input-copy-focus-ring`, `--input-copy-description-line-clamp`.
18
+ > - The input's focus outline used the border-radius token as its colour; it's now a real
19
+ > `:focus-visible` ring from `--input-copy-focus-ring`.
20
+ > - The button label is `InputButton`'s `.button-text` (was `.input-copy__button-text`), and it
21
+ > hides below 48rem of the **component's** width (a container query), not a 480px viewport.
22
+
3
23
  ## Public token API
4
24
 
5
- All `--input-copy-*` tokens are the stable override surface. Set them at any scope (global, page,
6
- or instance) without touching the component itself.
25
+ ### Layout
7
26
 
8
27
  | Token | Default | Controls |
9
28
  |---|---|---|
10
- | `--input-copy-input-bg` | `var(--slate-00)` | Input field background colour |
11
- | `--input-copy-input-border` | `1px solid var(--slate-03)` | Input field border |
12
- | `--input-copy-input-border-radius` | `0.4rem` | Input field corner rounding |
13
- | `--input-copy-input-padding` | `0.8rem 1rem` | Input field padding |
14
- | `--input-copy-input-text-color` | `#333` | Input text colour |
15
- | `--input-copy-input-placeholder-color` | `#999` | Placeholder text colour |
16
- | `--input-copy-input-font-size` | `0.95rem` | Input field font size |
17
- | `--input-copy-button-bg` | `var(--input-button-primary-surface)` | Copy button background |
18
- | `--input-copy-button-bg-hover` | `var(--input-button-primary-surface-hover)` | Copy button background on hover |
19
- | `--input-copy-button-text-color` | `var(--input-button-primary-text)` | Copy button text colour |
20
- | `--input-copy-button-padding` | `0.8rem 1.2rem` | Copy button padding |
21
- | `--input-copy-button-border-radius` | `0.4rem` | Copy button corner rounding |
22
- | `--input-copy-button-gap` | `0.6rem` | Gap between icon and text in button |
23
- | `--input-copy-description-color` | `#666` | Description text colour |
24
- | `--input-copy-description-font-size` | `0.85rem` | Description font size |
25
- | `--input-copy-description-margin` | `0.6rem 0 0 0` | Description margin |
26
-
27
- ---
29
+ | `--input-copy-gap` | `0.8rem` | Gap between input and button, and above the description |
28
30
 
29
- ## Global theming
31
+ ### Input
32
+
33
+ | Token | Default | Controls |
34
+ |---|---|---|
35
+ | `--input-copy-input-bg` | `var(--theme-input-surface)` | Input background |
36
+ | `--input-copy-input-border` | `0.1rem solid var(--theme-border)` | Input border (shorthand) |
37
+ | `--input-copy-input-border-radius` | `0.4rem` | Input corner radius |
38
+ | `--input-copy-input-padding` | `0.8rem 1rem` | Input padding |
39
+ | `--input-copy-input-text-color` | `var(--theme-text)` | Input text colour |
40
+ | `--input-copy-input-font-size` | `var(--step-4)` | Input and button font size |
41
+ | `--input-copy-focus-ring` | `var(--theme-border-focus)` | `:focus-visible` outline colour on the input and the button |
42
+
43
+ ### Button
30
44
 
31
- Create `assets/styles/setup/07.components/input-copy.css` in the consuming app and set tokens on
32
- `:root`. These values apply to every `InputCopy` instance across the site.
45
+ | Token | Default | Controls |
46
+ |---|---|---|
47
+ | `--input-copy-button-bg` | `var(--input-button-primary-surface, var(--theme-surface))` | Button background |
48
+ | `--input-copy-button-bg-hover` | `var(--input-button-primary-surface-hover, var(--theme-surface-hover))` | Button background on hover |
49
+ | `--input-copy-button-text-color` | `var(--input-button-primary-text, var(--theme-on-surface))` | Button text and icon colour |
50
+ | `--input-copy-button-padding` | `0.8rem 1.2rem` | Button padding |
51
+ | `--input-copy-button-border-radius` | `0.4rem` | Button corner radius |
52
+ | `--input-copy-button-gap` | `0.6rem` | Gap between icon and label |
53
+
54
+ ### Description
55
+
56
+ | Token | Default | Controls |
57
+ |---|---|---|
58
+ | `--input-copy-description-color` | `var(--theme-text)` | Description text colour |
59
+ | `--input-copy-description-font-size` | `var(--step-4)` | Description font size |
60
+ | `--input-copy-description-margin` | `0` | Description margin |
61
+ | `--input-copy-description-line-clamp` | `none` | Lines before the description is cut off with an ellipsis (`1` = single line) |
62
+
63
+ Private tokens (not public API): `--_input-font-size` on the root, the resolved input font size
64
+ shared by the input and the button.
65
+
66
+ ## State hooks
67
+
68
+ - `.input-copy-copied` on the root for `copiedDuration` ms after a successful copy. The button then
69
+ has `aria-disabled="true"` (it stays focusable and ignores clicks) and shows `copiedText` and
70
+ `copiedIcon`.
71
+ - `disabled` on the button when `isDisabled` is set.
72
+ - Classes: `.input-copy` > `.input-copy__wrapper` > `.input-copy__input` and
73
+ `.input-copy__button` (also `.input-button.inline`, containing `.input-copy__icon` and
74
+ `.button-text`); then `.input-copy__description` and a visually hidden `role="status"` span.
75
+ - Container: the root is `container: input-copy / inline-size`. Below `48rem` of its own width the
76
+ button's `.button-text` is hidden and only the icon shows (the button keeps its aria-label).
77
+
78
+ ## Global theming
33
79
 
34
80
  ```css
35
- /* assets/styles/setup/07.components/input-copy.css */
36
- :root {
81
+ :where(html) {
37
82
  --input-copy-input-border-radius: 0.6rem;
38
- --input-copy-input-padding: 1rem 1.2rem;
39
83
  --input-copy-button-border-radius: 0.6rem;
40
- --input-copy-button-gap: 0.8rem;
41
- --input-copy-description-color: var(--slate-06);
42
84
  }
43
85
  ```
44
86
 
45
- ---
46
-
47
87
  ## Local overrides
48
88
 
49
89
  Set the tokens above on an element you own (a page or section class, or a class added with
50
90
  `:style-class-passthrough`): they inherit down into the component. Keep the block **unlayered** (no
51
91
  `@layer` wrapper) so it beats the library's `@layer components`. If your own file uses
52
- `<style scoped>`, tokens set on your element still work, but selectors that reach inside the component need
53
- `:deep()`. Patterns and examples: `.claude/skills/component-local-style-override.md`.
54
-
55
- ### One instance
56
-
57
- Override tokens directly on a single InputCopy instance:
58
-
59
- ```vue
60
- <InputCopy
61
- value="sk_live_abc123"
62
- label="API key"
63
- style="
64
- --input-copy-input-border-radius: 0.8rem;
65
- --input-copy-button-bg: var(--brand-success);
66
- "
67
- />
68
- ```
92
+ `<style scoped>`, tokens set on your element still work, but selectors that reach inside the
93
+ component need `:deep()`. Patterns and examples: `.claude/skills/component-local-style-override.md`.
69
94
 
70
- ### Page or section
95
+ **Caveat:** the button sets `--input-button-text-white-space: normal` on itself so long labels wrap,
96
+ so a value set on an ancestor won't reach it.
71
97
 
72
- When input-copy appears in a specific page context (e.g., checkout success page), scope overrides
73
- under the page wrapper:
98
+ ## Class passthrough
74
99
 
75
- ```css
76
- /* In the consuming page's unscoped <style> block */
77
- .success-page {
78
- .input-copy {
79
- --input-copy-input-bg: var(--brand-surface-light);
80
- --input-copy-input-border: 2px solid var(--brand-primary);
81
- --input-copy-button-bg: var(--brand-primary);
82
- --input-copy-button-bg-hover: var(--brand-primary-hover);
83
- }
84
- }
85
- ```
86
-
87
- ---
88
-
89
- ## License key display pattern
100
+ `style-class-passthrough` adds classes to the root `.input-copy`, next to `.input-copy-copied`. It's
101
+ reachable in normal use and is a good place to set tokens for one instance.
90
102
 
91
- Typical usage on a success page or dashboard to display a license key:
92
-
93
- ```vue
94
- <InputCopy
95
- :value="licenseKey"
96
- label="License key"
97
- description="Your license key is ready to use. Copy it and add to your component."
98
- buttonText="Copy"
99
- copiedText="Copied!"
100
- @copied="showFeedback"
101
- />
102
- ```
103
-
104
- ```css
105
- .success-page {
106
- .input-copy {
107
- max-width: 500px;
108
-
109
- --input-copy-input-bg: #f9f9f9;
110
- --input-copy-input-border: 1px solid var(--slate-02);
111
- --input-copy-input-text-color: var(--slate-09);
112
- }
113
- }
114
- ```
115
-
116
- ---
117
-
118
- ## Readonly input styling
119
-
120
- The input field is always readonly to prevent accidental edits. Style it distinctly if needed:
121
-
122
- ```css
123
- .input-copy {
124
- .input-copy__input {
125
- background-color: var(--slate-00);
126
- color: var(--slate-09);
127
- cursor: default;
128
- }
129
- }
130
- ```
131
-
132
- ---
133
-
134
- ## Button text visibility on small screens
135
-
136
- By default, the button text is hidden on screens < 480px. Override for always-visible text:
137
-
138
- ```css
139
- .input-copy__button-text {
140
- display: inline !important;
141
- }
142
- ```
143
-
144
- Or show only the icon on mobile and text on desktop (default behavior):
145
-
146
- ```css
147
- /* Default: button shows only icon on mobile, icon + text on desktop (480px+) */
148
- /* No override needed; this is the built-in behavior */
149
- ```
103
+ ## Notes
150
104
 
105
+ - The root is an inline-size container, so it can't size itself from its content: give it a width
106
+ from its parent (it fills a block or grid cell; inside a flex row, give it `flex: 1` or a width).
107
+ - The button is capped at half the row (`max-inline-size: 50%`) and its label wraps, so a long
108
+ translated label can't squeeze the input to nothing.
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="input-copy" :class="elementClasses">
2
+ <div class="input-copy" :class="[{ 'input-copy-copied': copied }, elementClasses]">
3
3
  <div class="input-copy__wrapper">
4
4
  <input
5
5
  :id="resolvedId"
@@ -8,20 +8,26 @@
8
8
  :name="name || resolvedId"
9
9
  :value="value"
10
10
  :aria-label="ariaLabel || label"
11
+ :aria-describedby="description ? descriptionId : undefined"
11
12
  readonly
12
13
  />
13
- <button
14
+ <InputButton
14
15
  type="button"
16
+ variant="inline"
15
17
  class="input-copy__button"
16
- :aria-label="`Copy ${ariaLabel || label || 'value'} to clipboard`"
17
- :disabled="copied || isDisabled"
18
+ :button-text="copied ? copiedText : buttonText"
19
+ :aria-label="resolvedButtonAriaLabel"
20
+ :disabled="isDisabled"
21
+ :readonly="copied"
18
22
  @click="handleCopy"
19
23
  >
20
- <Icon name="lucide:copy" class="input-copy__icon" />
21
- <span class="input-copy__button-text">{{ copied ? copiedText : buttonText }}</span>
22
- </button>
24
+ <template #left>
25
+ <Icon :name="copied ? copiedIcon : icon" class="input-copy__icon" />
26
+ </template>
27
+ </InputButton>
23
28
  </div>
24
- <p v-if="description" class="input-copy__description">{{ description }}</p>
29
+ <p v-if="description" :id="descriptionId" class="input-copy__description">{{ description }}</p>
30
+ <span class="sr-only" role="status">{{ copied ? copiedText : "" }}</span>
25
31
  </div>
26
32
  </template>
27
33
 
@@ -36,6 +42,9 @@ interface Props {
36
42
  description?: string;
37
43
  buttonText?: string;
38
44
  copiedText?: string;
45
+ buttonAriaLabel?: string;
46
+ icon?: string;
47
+ copiedIcon?: string;
39
48
  copiedDuration?: number;
40
49
  isDisabled?: boolean;
41
50
  styleClassPassthrough?: string | string[];
@@ -49,17 +58,23 @@ const props = withDefaults(defineProps<Props>(), {
49
58
  description: undefined,
50
59
  buttonText: "Copy",
51
60
  copiedText: "Copied!",
61
+ buttonAriaLabel: undefined,
62
+ icon: "lucide:copy",
63
+ copiedIcon: "lucide:check",
52
64
  copiedDuration: 2000,
53
65
  isDisabled: false,
54
66
  styleClassPassthrough: () => [],
55
67
  });
56
68
 
57
69
  // Readonly text inputs still need an id/name to satisfy "a form field element should have an
58
- // id or name attribute" — auto-generated so every existing call site (none of which pass one)
59
- // gets a valid, unique id for free. Named resolvedId, not id, so it can't be confused with (or
60
- // silently shadow) the `id` prop it falls back to.
70
+ // id or name attribute", so one is auto-generated when none is passed.
61
71
  const autoId = useId();
62
72
  const resolvedId = computed(() => props.id ?? autoId);
73
+ const descriptionId = computed(() => `${resolvedId.value}-description`);
74
+
75
+ const resolvedButtonAriaLabel = computed(
76
+ () => props.buttonAriaLabel ?? `Copy ${props.ariaLabel || props.label || "value"} to clipboard`
77
+ );
63
78
 
64
79
  const emit = defineEmits<{
65
80
  copy: [value: string];
@@ -95,7 +110,7 @@ watch(
95
110
  () => props.styleClassPassthrough,
96
111
  () => {
97
112
  resetElementClasses(props.styleClassPassthrough);
98
- },
113
+ }
99
114
  );
100
115
 
101
116
  onUnmounted(() => {
@@ -108,95 +123,105 @@ onUnmounted(() => {
108
123
  <style lang="css">
109
124
  @layer components {
110
125
  .input-copy {
111
- --_input-border-radius: var(--input-copy-input-border-radius, 0.4rem);
112
- --_input-font-size: var(--input-copy-input-font-size, 0.95rem);
113
-
114
- --_button-bg-hover: var(
115
- --input-copy-button-bg-hover,
116
- var(--input-button-primary-surface-hover, var(--theme-surface-hover))
117
- );
118
- --_button-text-color: var(
119
- --input-copy-button-text-color,
120
- var(--input-button-primary-text, var(--theme-on-surface))
121
- );
126
+ --_input-font-size: var(--input-copy-input-font-size, var(--step-4));
122
127
 
128
+ container: input-copy / inline-size;
123
129
  display: flex;
124
130
  flex-direction: column;
125
- gap: 0.8rem;
126
- }
131
+ gap: var(--input-copy-gap, 0.8rem);
127
132
 
128
- .input-copy__wrapper {
129
- display: flex;
130
- gap: 0.8rem;
131
- align-items: stretch;
132
- }
133
+ .input-copy__wrapper {
134
+ display: flex;
135
+ gap: var(--input-copy-gap, 0.8rem);
136
+ align-items: stretch;
137
+ }
133
138
 
134
- .input-copy__input {
135
- flex: 1;
136
- min-width: 0;
139
+ .input-copy__input {
140
+ flex: 1;
141
+ min-width: 0;
142
+ padding: var(--input-copy-input-padding, 0.8rem 1rem);
143
+ background-color: var(--input-copy-input-bg, var(--theme-input-surface));
144
+ border: var(--input-copy-input-border, 0.1rem solid var(--theme-border));
145
+ border-radius: var(--input-copy-input-border-radius, 0.4rem);
146
+ font-size: var(--_input-font-size);
147
+ color: var(--input-copy-input-text-color, var(--theme-text));
148
+ font-family: inherit;
149
+
150
+ &:focus-visible {
151
+ outline: var(--form-element-outline-width-focus, 0.2rem) solid
152
+ var(--input-copy-focus-ring, var(--theme-border-focus));
153
+ outline-offset: var(--form-element-outline-offset-focus, 0.2rem);
154
+ }
155
+ }
137
156
 
138
- padding: var(--input-copy-input-padding, 0.8rem 1rem);
139
- background-color: var(--input-copy-input-bg, var(--slate-00));
140
- border: var(--input-copy-input-border, 1px solid var(--slate-03));
141
- border-radius: var(--_input-border-radius);
142
- font-size: var(--_input-font-size);
143
- color: var(--input-copy-input-text-color, #333);
144
- font-family: inherit;
157
+ .input-copy__button.input-button {
158
+ flex-shrink: 0;
159
+ max-inline-size: 50%;
160
+ box-sizing: border-box;
161
+ min-height: auto;
162
+ gap: var(--input-copy-button-gap, 0.6rem);
163
+ padding: var(--input-copy-button-padding, 0.8rem 1.2rem);
164
+ background-color: var(--input-copy-button-bg, var(--input-button-primary-surface, var(--theme-surface)));
165
+ color: var(--input-copy-button-text-color, var(--input-button-primary-text, var(--theme-on-surface)));
166
+ border: none;
167
+ border-radius: var(--input-copy-button-border-radius, 0.4rem);
168
+ font-size: var(--_input-font-size);
169
+ font-weight: 500;
170
+ cursor: pointer;
171
+ transition: background-color 0.2s ease;
172
+ --input-button-text-white-space: normal;
173
+
174
+ &:hover:not(:disabled, [aria-disabled="true"]) {
175
+ background-color: var(
176
+ --input-copy-button-bg-hover,
177
+ var(--input-button-primary-surface-hover, var(--theme-surface-hover))
178
+ );
179
+ }
145
180
 
146
- &::placeholder {
147
- color: var(--input-copy-input-placeholder-color, #999);
148
- }
181
+ &:focus-visible {
182
+ outline: var(--form-element-outline-width-focus, 0.2rem) solid
183
+ var(--input-copy-focus-ring, var(--theme-border-focus));
184
+ outline-offset: var(--form-element-outline-offset-focus, 0.2rem);
185
+ }
149
186
 
150
- &:focus {
151
- outline: 2px solid var(--_input-border-radius);
152
- outline-offset: 2px;
153
- }
154
- }
187
+ &:disabled {
188
+ opacity: 0.6;
189
+ cursor: not-allowed;
190
+ }
155
191
 
156
- .input-copy__button {
157
- display: flex;
158
- align-items: center;
159
- gap: var(--input-copy-button-gap, 0.6rem);
160
- padding: var(--input-copy-button-padding, 0.8rem 1.2rem);
161
-
162
- background-color: var(--input-copy-button-bg, var(--input-button-primary-surface, var(--theme-surface)));
163
- color: var(--_button-text-color);
164
- border: none;
165
- border-radius: var(--input-copy-button-border-radius, 0.4rem);
166
- font-size: var(--_input-font-size);
167
- font-weight: 500;
168
- cursor: pointer;
169
- white-space: nowrap;
170
- transition: background-color 0.2s ease;
171
-
172
- &:hover:not(:disabled) {
173
- background-color: var(--_button-bg-hover);
174
- }
192
+ &[aria-disabled="true"] {
193
+ cursor: default;
194
+ }
175
195
 
176
- &:disabled {
177
- opacity: 0.6;
178
- cursor: not-allowed;
179
- }
196
+ .input-copy__icon {
197
+ width: 1.2em;
198
+ height: 1.2em;
199
+ flex-shrink: 0;
200
+ }
180
201
 
181
- .input-copy__icon {
182
- width: 1.2em;
183
- height: 1.2em;
184
- flex-shrink: 0;
202
+ .button-text {
203
+ font-size: inherit;
204
+ text-transform: none;
205
+ }
185
206
  }
186
207
 
187
- .input-copy__button-text {
188
- display: none;
189
-
190
- @media (min-width: 480px) {
191
- display: inline;
208
+ @container input-copy (width < 48rem) {
209
+ .input-copy__button.input-button .button-text {
210
+ display: none;
192
211
  }
193
212
  }
194
- }
195
213
 
196
- .input-copy__description {
197
- margin: var(--input-copy-description-margin, 0.6rem 0 0 0);
198
- font-size: var(--input-copy-description-font-size, 0.85rem);
199
- color: var(--input-copy-description-color, #666);
214
+ .input-copy__description {
215
+ margin: var(--input-copy-description-margin, 0);
216
+ font-size: var(--input-copy-description-font-size, var(--step-4));
217
+ color: var(--input-copy-description-color, var(--theme-text));
218
+ display: -webkit-box;
219
+ -webkit-box-orient: vertical;
220
+ overflow: hidden;
221
+ overflow-wrap: anywhere;
222
+ -webkit-line-clamp: var(--input-copy-description-line-clamp, none);
223
+ line-clamp: var(--input-copy-description-line-clamp, none);
224
+ }
200
225
  }
201
226
  }
202
227
  </style>