@fpkit/acss 3.7.0 → 3.9.0

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.
@@ -1,7 +1,9 @@
1
1
  import { StoryObj, Meta } from "@storybook/react-vite";
2
2
  import { within, userEvent, expect } from "storybook/test";
3
+ import React from "react";
3
4
 
4
5
  import Input from "./inputs";
6
+ import { Checkbox as CheckboxComponent } from "./checkbox";
5
7
  import "./form.scss";
6
8
 
7
9
  const meta: Meta<typeof Input> = {
@@ -222,6 +224,254 @@ export const UrlInput: Story = {
222
224
  },
223
225
  } as Story;
224
226
 
227
+ export const Checkbox: Story = {
228
+ args: {
229
+ type: "checkbox",
230
+ },
231
+ play: async ({ canvasElement }) => {
232
+ const canvas = within(canvasElement);
233
+ const input = canvas.getByRole("checkbox");
234
+ expect(input).toHaveAttribute("type", "checkbox");
235
+
236
+ await userEvent.click(input);
237
+ expect(input).toBeChecked();
238
+
239
+ await userEvent.click(input);
240
+ expect(input).not.toBeChecked();
241
+ },
242
+ } as Story;
243
+
244
+ // ============================================================================
245
+ // Checkbox Wrapper Component Stories
246
+ // ============================================================================
247
+
248
+ /**
249
+ * CheckboxWrapper - Basic checkbox with label
250
+ *
251
+ * Demonstrates the Checkbox wrapper component with simplified API.
252
+ * Features automatic label association, boolean onChange, and keyboard support.
253
+ */
254
+ export const CheckboxWrapper: Story = {
255
+ render: () => (
256
+ <CheckboxComponent id="basic" label="I accept the terms and conditions" />
257
+ ),
258
+ play: async ({ canvasElement, step }) => {
259
+ const canvas = within(canvasElement);
260
+ const checkbox = canvas.getByRole("checkbox");
261
+
262
+ await step("Checkbox renders unchecked", async () => {
263
+ expect(checkbox).toBeInTheDocument();
264
+ expect(checkbox).not.toBeChecked();
265
+ });
266
+
267
+ await step("Checkbox can be checked by clicking", async () => {
268
+ await userEvent.click(checkbox);
269
+ expect(checkbox).toBeChecked();
270
+ });
271
+
272
+ await step("Label can be clicked to toggle", async () => {
273
+ const label = canvas.getByText("I accept the terms and conditions");
274
+ await userEvent.click(label);
275
+ expect(checkbox).not.toBeChecked();
276
+ });
277
+
278
+ await step("Space key toggles checkbox", async () => {
279
+ checkbox.focus();
280
+ await userEvent.keyboard(" ");
281
+ expect(checkbox).toBeChecked();
282
+ });
283
+ },
284
+ };
285
+
286
+ /**
287
+ * CheckboxControlled - Controlled checkbox with state management
288
+ *
289
+ * Demonstrates controlled mode with React state and boolean onChange API.
290
+ */
291
+ const CheckboxControlledExample = () => {
292
+ const [checked, setChecked] = React.useState(false);
293
+ return (
294
+ <div>
295
+ <CheckboxComponent
296
+ id="controlled"
297
+ label="Subscribe to newsletter"
298
+ checked={checked}
299
+ onChange={setChecked}
300
+ />
301
+ <p style={{ marginTop: "1rem", fontSize: "0.875rem", color: "#6b7280" }}>
302
+ Status: {checked ? "✓ Subscribed" : "Not subscribed"}
303
+ </p>
304
+ </div>
305
+ );
306
+ };
307
+
308
+ export const CheckboxControlled: Story = {
309
+ render: () => <CheckboxControlledExample />,
310
+ };
311
+
312
+ /**
313
+ * CheckboxRequired - Required checkbox with asterisk indicator
314
+ *
315
+ * Shows required field indicator and aria-required attribute.
316
+ */
317
+ export const CheckboxRequired: Story = {
318
+ render: () => (
319
+ <CheckboxComponent
320
+ id="required"
321
+ label="I accept the terms"
322
+ required
323
+ />
324
+ ),
325
+ play: async ({ canvasElement }) => {
326
+ const canvas = within(canvasElement);
327
+ const checkbox = canvas.getByRole("checkbox");
328
+ expect(checkbox).toHaveAttribute("aria-required", "true");
329
+ expect(canvas.getByText("*")).toBeInTheDocument();
330
+ },
331
+ };
332
+
333
+ /**
334
+ * CheckboxDisabled - Disabled checkbox (WCAG compliant)
335
+ *
336
+ * Demonstrates aria-disabled pattern that remains focusable for screen readers.
337
+ */
338
+ export const CheckboxDisabled: Story = {
339
+ render: () => (
340
+ <CheckboxComponent
341
+ id="disabled"
342
+ label="Disabled option"
343
+ disabled
344
+ defaultChecked
345
+ />
346
+ ),
347
+ play: async ({ canvasElement, step }) => {
348
+ const canvas = within(canvasElement);
349
+ const checkbox = canvas.getByRole("checkbox");
350
+
351
+ await step("Disabled checkbox has aria-disabled", async () => {
352
+ expect(checkbox).toHaveAttribute("aria-disabled", "true");
353
+ });
354
+
355
+ await step("Disabled checkbox remains focusable", async () => {
356
+ await userEvent.tab();
357
+ expect(checkbox).toHaveFocus();
358
+ });
359
+
360
+ await step("Disabled checkbox prevents interaction", async () => {
361
+ const wasChecked = checkbox.checked;
362
+ await userEvent.click(checkbox);
363
+ // Value should remain unchanged due to disabled state
364
+ expect(checkbox.checked).toBe(wasChecked);
365
+ });
366
+ },
367
+ };
368
+
369
+ /**
370
+ * CheckboxValidation - Checkbox with validation error
371
+ *
372
+ * Shows error state with aria-invalid and error message.
373
+ */
374
+ export const CheckboxValidation: Story = {
375
+ render: () => (
376
+ <CheckboxComponent
377
+ id="validation"
378
+ label="I accept the terms"
379
+ validationState="invalid"
380
+ errorMessage="You must accept the terms to continue"
381
+ />
382
+ ),
383
+ play: async ({ canvasElement }) => {
384
+ const canvas = within(canvasElement);
385
+ const checkbox = canvas.getByRole("checkbox");
386
+ expect(checkbox).toHaveAttribute("aria-invalid", "true");
387
+ expect(checkbox).toHaveAttribute("aria-describedby");
388
+ expect(canvas.getByText("You must accept the terms to continue")).toBeInTheDocument();
389
+ },
390
+ };
391
+
392
+ /**
393
+ * CheckboxWithHint - Checkbox with hint text
394
+ *
395
+ * Demonstrates hint text for additional context.
396
+ */
397
+ export const CheckboxWithHint: Story = {
398
+ render: () => (
399
+ <CheckboxComponent
400
+ id="hint"
401
+ label="Enable two-factor authentication"
402
+ hintText="Adds an extra layer of security to your account"
403
+ />
404
+ ),
405
+ };
406
+
407
+ /**
408
+ * CheckboxCustomSize - Custom sized checkboxes using CSS variables
409
+ *
410
+ * Demonstrates responsive sizing via --checkbox-size variable.
411
+ */
412
+ export const CheckboxCustomSize: Story = {
413
+ render: () => (
414
+ <div style={{ display: "flex", gap: "1.5rem", flexDirection: "column" }}>
415
+ <CheckboxComponent
416
+ id="small"
417
+ label="Small (1rem)"
418
+ styles={{ "--checkbox-gap": "0.375rem" } as React.CSSProperties}
419
+ />
420
+ <CheckboxComponent
421
+ id="medium"
422
+ label="Medium (1.25rem - default)"
423
+ />
424
+ <CheckboxComponent
425
+ id="large"
426
+ label="Large (1.5rem)"
427
+ styles={{ "--checkbox-gap": "0.75rem" } as React.CSSProperties}
428
+ />
429
+ <CheckboxComponent
430
+ id="xlarge"
431
+ label="Extra Large (2rem)"
432
+ styles={{ "--checkbox-gap": "1rem" } as React.CSSProperties}
433
+ />
434
+ </div>
435
+ ),
436
+ };
437
+
438
+ /**
439
+ * CheckboxGroup - Multiple checkboxes in a fieldset
440
+ *
441
+ * Demonstrates grouping related checkboxes with semantic HTML.
442
+ */
443
+ export const CheckboxGroup: Story = {
444
+ render: () => (
445
+ <fieldset style={{ border: "1px solid #d1d5db", padding: "1.5rem", borderRadius: "0.5rem" }}>
446
+ <legend style={{ fontWeight: "600", fontSize: "1rem", padding: "0 0.5rem" }}>
447
+ Notification Preferences
448
+ </legend>
449
+ <div style={{ display: "flex", flexDirection: "column", gap: "1rem", marginTop: "1rem" }}>
450
+ <CheckboxComponent
451
+ id="email"
452
+ name="notifications"
453
+ value="email"
454
+ label="Email notifications"
455
+ defaultChecked
456
+ />
457
+ <CheckboxComponent
458
+ id="sms"
459
+ name="notifications"
460
+ value="sms"
461
+ label="SMS notifications"
462
+ />
463
+ <CheckboxComponent
464
+ id="push"
465
+ name="notifications"
466
+ value="push"
467
+ label="Push notifications"
468
+ defaultChecked
469
+ />
470
+ </div>
471
+ </fieldset>
472
+ ),
473
+ };
474
+
225
475
  /**
226
476
  * CSS Variable Customization
227
477
  *
@@ -237,7 +487,14 @@ export const UrlInput: Story = {
237
487
  */
238
488
  export const Customization: Story = {
239
489
  render: () => (
240
- <div style={{ display: "flex", flexDirection: "column", gap: "2rem", maxWidth: "600px" }}>
490
+ <div
491
+ style={{
492
+ display: "flex",
493
+ flexDirection: "column",
494
+ gap: "2rem",
495
+ maxWidth: "600px",
496
+ }}
497
+ >
241
498
  {/* Custom brand styling */}
242
499
  <div>
243
500
  <h4>Custom Brand Styling</h4>
package/src/index.scss CHANGED
@@ -23,6 +23,7 @@
23
23
  @use "./components/badge/badge.scss";
24
24
  @use "./components/nav/nav.scss";
25
25
  @use "./components/form/form.scss";
26
+ // @use "./components/checkbox/checkbox.scss"; // Deprecated - checkbox styles now in form/checkbox.scss (imported by form.scss)
26
27
  @use "./components/breadcrumbs/breadcrumb.scss";
27
28
  @use "./components/list/list.scss";
28
29
  @use "./components/alert/alert.scss";
package/src/index.ts CHANGED
@@ -63,6 +63,13 @@ export {
63
63
  export { Alert, type AlertProps } from "./components/alert/alert";
64
64
  export { Field, type FieldProps } from "./components/form/fields";
65
65
  export { Input, type InputProps } from "./components/form/inputs";
66
+
67
+ /**
68
+ * Checkbox wrapper component (uses Input type="checkbox")
69
+ * This is the recommended checkbox component.
70
+ */
71
+ export { Checkbox, type CheckboxProps } from "./components/form/checkbox";
72
+
66
73
  export { Icon, type IconProps } from "./components/icons/icon";
67
74
  export { Img } from "./components/images/img";
68
75
  export type { ImgProps } from "./components/images/img.types";
@@ -121,7 +128,12 @@ export * from "./components/layout/landmarks";
121
128
  export { Box, type BoxProps } from "./components/box/box";
122
129
  export { Stack, type StackProps } from "./components/stack/stack";
123
130
  export { Cluster, type ClusterProps } from "./components/cluster/cluster";
124
- export { default as Grid, GridItem, type GridProps, type GridItemProps } from "./components/grid/grid";
131
+ export {
132
+ default as Grid,
133
+ GridItem,
134
+ type GridProps,
135
+ type GridItemProps,
136
+ } from "./components/grid/grid";
125
137
  export { Row, type RowProps } from "./components/row/row";
126
138
  export { Col, type ColProps } from "./components/col/col";
127
139
  export { default as Flex } from "./components/flexbox/flex";
@@ -36,10 +36,10 @@
36
36
  * accessed via JavaScript. SCSS uses literal values for media queries.
37
37
  */
38
38
  :root {
39
- --col-breakpoint-xs: 0rem; /* 0px - base mobile */
40
- --col-breakpoint-sm: 30rem; /* 480px - large phones */
41
- --col-breakpoint-md: 48rem; /* 768px - tablets */
42
- --col-breakpoint-lg: 64rem; /* 1024px - desktops */
39
+ --col-breakpoint-xs: 0rem; /* 0px - base mobile */
40
+ --col-breakpoint-sm: 30rem; /* 480px - large phones */
41
+ --col-breakpoint-md: 48rem; /* 768px - tablets */
42
+ --col-breakpoint-lg: 64rem; /* 1024px - desktops */
43
43
 
44
44
  /* Legacy support - keep for backward compatibility */
45
45
  --col-breakpoint: var(--col-breakpoint-md);
@@ -51,9 +51,9 @@
51
51
  * @media queries require compile-time values, not runtime CSS variables.
52
52
  */
53
53
  $col-breakpoints: (
54
- 'sm': 30rem,
55
- 'md': 48rem,
56
- 'lg': 64rem,
54
+ "sm": 30rem,
55
+ "md": 48rem,
56
+ "lg": 64rem,
57
57
  );
58
58
 
59
59
  /* ============================================================================
@@ -432,8 +432,12 @@ $col-breakpoints: (
432
432
  */
433
433
  @each $breakpoint, $min-width in $col-breakpoints {
434
434
  @media (width >= #{$min-width}) {
435
- .col-#{$breakpoint}-order-first { order: -1; }
436
- .col-#{$breakpoint}-order-last { order: 13; }
435
+ .col-#{$breakpoint}-order-first {
436
+ order: -1;
437
+ }
438
+ .col-#{$breakpoint}-order-last {
439
+ order: 13;
440
+ }
437
441
 
438
442
  @for $i from 0 through 12 {
439
443
  .col-#{$breakpoint}-order-#{$i} {
@@ -0,0 +1 @@
1
+ {"version":3,"sourceRoot":"","sources":["../../components/checkbox/checkbox.scss"],"names":[],"mappings":"AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAeA;AACE;AAAA;AAAA;EAIA;EACA;EACA;AAEA;AAAA;AAAA;EAIA;EACA;EACA;EACA;EACA;EACA;EACA;AAEA;AAAA;AAAA;EAIA;EACA;EACA;AAEA;AAAA;AAAA;EAIA;EACA;EACA;EACA;EACA;AAEA;AAAA;AAAA;EAIA;EACA;EACA;EACA;AAAA;AAAA;AAIA;AAAA;AAAA;EAIA;EACA;EACA;EACA;AAAA;AAAA;AAIA;AAAA;AAAA;EAIA;EACA;AAEA;AAAA;AAAA;AAIA;EACA;EACA;EACA;EACA;AAEA;EACA;EACA;EACA;EACA;AAEA;EACA;EACA;EACA;EACA;AAEA;EACA;EACA;EACA;EACA;AAEA;AAAA;AAAA;AAIA;EACA;EACA;AAEA;EACA;EACA;EACA;AAEA;EACA;EACA;EACA;EACA;AAEA;EACA;EACA;EACA;EACA;EACA;AAEA;EACA;EACA;EACA;EACA;AAEA;EACA;EACA;;;AAGF;AAAA;AAAA;AAIA;EACE;EACA;EACA;AAEA;AAgBA;;AAfA;EACE;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;AAIF;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;AAAA;AAAA;EAGA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;AAAA;AAAA;EAGA;EACA;;;AAIJ;AAAA;AAAA;AAIA;EACE;EACA;EACA;EACA;;;AAGF;AAAA;AAAA;AAIA;EACE;EACA;EACA;EACA;EACA;;;AAGF;AAAA;AAAA;AAIA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;AAEA;AAOA;AAMA;AAeA;AAcA;AAaA;;AAtDA;EACE;EACA;EACA;;AAIF;EACE;EACA;;AAIF;EACE;EACA;;AAEA;EACE;EACA;;AAGF;EACE;;AAKJ;EACE;EACA;;AAEA;EACE;;AAGF;EACE;;AAKJ;EAEE;;AAEA;EACE;EACA;EACA;EACA;;AAKJ;EACE;;AAGF;EACE;;;AAIJ;AAAA;AAAA;AAIA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;AAAA;AAAA;AAIA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;AAAA;AAAA;AAIA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;AAAA;AAAA;AAIA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;AAEA;EACA;;;AAGF;AACA;EACE;EACA;;;AAGF;AAAA;AAAA;AAIA;EACE;EACA;EACA;EACA;EACA;;;AAGF;AAAA;AAAA;AAIA;EACE;EACA;EACA;EACA;EACA;;;AAGF;AAAA;AAAA;AAKE;EACE;EACA;;AAGF;EACE;;AAGF;AAAA;EAEE","file":"checkbox.css"}
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Checkbox Wrapper Component Styles
3
+ *
4
+ * Modern CSS architecture using :has() selector with ARIA attributes.
5
+ * No JavaScript class management required - ARIA attributes drive both
6
+ * accessibility AND styling.
7
+ *
8
+ * CSS Custom Properties:
9
+ * - --checkbox-gap: Space between checkbox and label (default: 0.5rem)
10
+ * - --checkbox-disabled-opacity: Opacity for disabled state (default: 0.6)
11
+ * - --checkbox-disabled-color: Label color when disabled (default: #6b7280)
12
+ * - --checkbox-label-fs: Label font size (default: 1rem)
13
+ * - --checkbox-label-lh: Label line height (default: 1.5)
14
+ * - --color-required: Required indicator color (default: #dc2626)
15
+ * - --checkbox-focus-ring-color: Focus ring color (default: #2563eb)
16
+ * - --checkbox-focus-ring-width: Focus ring width (default: 0.125rem)
17
+ * - --checkbox-focus-ring-offset: Focus ring offset (default: 0.125rem)
18
+ * - --checkbox-hover-label-color: Label color on hover (default: inherit)
19
+ * - --checkbox-error-label-color: Label color when invalid (default: #dc2626)
20
+ * - --checkbox-valid-label-color: Label color when valid (default: #16a34a)
21
+ * - --checkbox-focus-radius: Focus outline border radius (default: 0.125rem)
22
+ *
23
+ * WCAG 2.1 AA Compliance:
24
+ * - 2.4.7 Focus Visible: Focus-visible indicators with sufficient contrast
25
+ * - 2.3.3 Animation from Interactions: Respects prefers-reduced-motion
26
+ * - 3.3.1 Error Identification: Visual error states with color + text
27
+ * - 4.1.2 Name, Role, Value: ARIA attributes for assistive technologies
28
+ * - 1.4.13 Content on Hover or Focus: Hover states for visual feedback
29
+ */
30
+ div:has(> input[type=checkbox]) {
31
+ display: flex;
32
+ align-items: center;
33
+ gap: var(--checkbox-gap, 0.5rem);
34
+ position: relative;
35
+ }
36
+ div:has(> input[type=checkbox]) > input[type=checkbox] {
37
+ flex-shrink: 0;
38
+ order: -1;
39
+ }
40
+ div:has(> input[type=checkbox]):not(:has(> input[aria-disabled=true])):hover .checkbox-label {
41
+ color: var(--checkbox-hover-label-color, inherit);
42
+ }
43
+ div:has(> input[type=checkbox]):has(> input:focus-visible) .checkbox-label {
44
+ outline: var(--checkbox-focus-ring-width, 0.125rem) solid var(--checkbox-focus-ring-color, #2563eb);
45
+ outline-offset: var(--checkbox-focus-ring-offset, 0.125rem);
46
+ border-radius: var(--checkbox-focus-radius, 0.125rem);
47
+ }
48
+ div:has(> input[type=checkbox]):has(> input[aria-disabled=true]) {
49
+ opacity: var(--checkbox-disabled-opacity, 0.6);
50
+ cursor: not-allowed;
51
+ }
52
+ div:has(> input[type=checkbox]):has(> input[aria-disabled=true]) .checkbox-label {
53
+ color: var(--checkbox-disabled-color, #6b7280);
54
+ cursor: not-allowed;
55
+ }
56
+ div:has(> input[type=checkbox]):has(> input[aria-invalid=true]) .checkbox-label {
57
+ color: var(--checkbox-error-label-color, #dc2626);
58
+ }
59
+ div:has(> input[type=checkbox]):has(> input[aria-invalid=false]:checked) .checkbox-label {
60
+ color: var(--checkbox-valid-label-color, #16a34a);
61
+ }
62
+
63
+ .checkbox-label {
64
+ cursor: pointer;
65
+ font-size: var(--checkbox-label-fs, 1rem);
66
+ line-height: var(--checkbox-label-lh, 1.5);
67
+ user-select: none;
68
+ margin: 0;
69
+ flex: 1;
70
+ min-width: 0;
71
+ transition: color 0.2s ease-in-out;
72
+ }
73
+ @media (prefers-reduced-motion: reduce) {
74
+ .checkbox-label {
75
+ transition: none;
76
+ }
77
+ }
78
+ .checkbox-label .checkbox-required {
79
+ color: var(--color-required, #dc2626);
80
+ font-weight: 600;
81
+ margin-inline-start: 0.125rem;
82
+ }
83
+
84
+ @media (forced-colors: active) {
85
+ .checkbox-input {
86
+ forced-color-adjust: auto;
87
+ }
88
+ }
89
+
90
+ @container (max-width: 400px) {
91
+ div:has(> input[type=checkbox]) {
92
+ flex-direction: column;
93
+ align-items: flex-start;
94
+ }
95
+ }
96
+
97
+ /*# sourceMappingURL=checkbox.css.map */
@@ -0,0 +1 @@
1
+ {"version":3,"sourceRoot":"","sources":["../../components/form/checkbox.scss"],"names":[],"mappings":"AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA+BA;EACE;EACA;EACA;EACA;;AAGA;EACE;EACA;;AAKA;EACE;;AAOF;EACE;EAEA;EACA;;AAMJ;EACE;EACA;;AAEA;EACE;EACA;;AAOF;EACE;;AAMF;EACE;;;AAMN;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGA;EAXF;IAYI;;;AAGF;EACE;EACA;EACA;;;AAQF;EAHF;IAII;;;;AAMJ;EACE;IACE;IACA","file":"checkbox.css"}
@@ -1,3 +1,98 @@
1
+ /**
2
+ * Checkbox Wrapper Component Styles
3
+ *
4
+ * Modern CSS architecture using :has() selector with ARIA attributes.
5
+ * No JavaScript class management required - ARIA attributes drive both
6
+ * accessibility AND styling.
7
+ *
8
+ * CSS Custom Properties:
9
+ * - --checkbox-gap: Space between checkbox and label (default: 0.5rem)
10
+ * - --checkbox-disabled-opacity: Opacity for disabled state (default: 0.6)
11
+ * - --checkbox-disabled-color: Label color when disabled (default: #6b7280)
12
+ * - --checkbox-label-fs: Label font size (default: 1rem)
13
+ * - --checkbox-label-lh: Label line height (default: 1.5)
14
+ * - --color-required: Required indicator color (default: #dc2626)
15
+ * - --checkbox-focus-ring-color: Focus ring color (default: #2563eb)
16
+ * - --checkbox-focus-ring-width: Focus ring width (default: 0.125rem)
17
+ * - --checkbox-focus-ring-offset: Focus ring offset (default: 0.125rem)
18
+ * - --checkbox-hover-label-color: Label color on hover (default: inherit)
19
+ * - --checkbox-error-label-color: Label color when invalid (default: #dc2626)
20
+ * - --checkbox-valid-label-color: Label color when valid (default: #16a34a)
21
+ * - --checkbox-focus-radius: Focus outline border radius (default: 0.125rem)
22
+ *
23
+ * WCAG 2.1 AA Compliance:
24
+ * - 2.4.7 Focus Visible: Focus-visible indicators with sufficient contrast
25
+ * - 2.3.3 Animation from Interactions: Respects prefers-reduced-motion
26
+ * - 3.3.1 Error Identification: Visual error states with color + text
27
+ * - 4.1.2 Name, Role, Value: ARIA attributes for assistive technologies
28
+ * - 1.4.13 Content on Hover or Focus: Hover states for visual feedback
29
+ */
30
+ div:has(> input[type=checkbox]) {
31
+ display: flex;
32
+ align-items: center;
33
+ gap: var(--checkbox-gap, 0.5rem);
34
+ position: relative;
35
+ }
36
+ div:has(> input[type=checkbox]) > input[type=checkbox] {
37
+ flex-shrink: 0;
38
+ order: -1;
39
+ }
40
+ div:has(> input[type=checkbox]):not(:has(> input[aria-disabled=true])):hover .checkbox-label {
41
+ color: var(--checkbox-hover-label-color, inherit);
42
+ }
43
+ div:has(> input[type=checkbox]):has(> input:focus-visible) .checkbox-label {
44
+ outline: var(--checkbox-focus-ring-width, 0.125rem) solid var(--checkbox-focus-ring-color, #2563eb);
45
+ outline-offset: var(--checkbox-focus-ring-offset, 0.125rem);
46
+ border-radius: var(--checkbox-focus-radius, 0.125rem);
47
+ }
48
+ div:has(> input[type=checkbox]):has(> input[aria-disabled=true]) {
49
+ opacity: var(--checkbox-disabled-opacity, 0.6);
50
+ cursor: not-allowed;
51
+ }
52
+ div:has(> input[type=checkbox]):has(> input[aria-disabled=true]) .checkbox-label {
53
+ color: var(--checkbox-disabled-color, #6b7280);
54
+ cursor: not-allowed;
55
+ }
56
+ div:has(> input[type=checkbox]):has(> input[aria-invalid=true]) .checkbox-label {
57
+ color: var(--checkbox-error-label-color, #dc2626);
58
+ }
59
+ div:has(> input[type=checkbox]):has(> input[aria-invalid=false]:checked) .checkbox-label {
60
+ color: var(--checkbox-valid-label-color, #16a34a);
61
+ }
62
+
63
+ .checkbox-label {
64
+ cursor: pointer;
65
+ font-size: var(--checkbox-label-fs, 1rem);
66
+ line-height: var(--checkbox-label-lh, 1.5);
67
+ user-select: none;
68
+ margin: 0;
69
+ flex: 1;
70
+ min-width: 0;
71
+ transition: color 0.2s ease-in-out;
72
+ }
73
+ @media (prefers-reduced-motion: reduce) {
74
+ .checkbox-label {
75
+ transition: none;
76
+ }
77
+ }
78
+ .checkbox-label .checkbox-required {
79
+ color: var(--color-required, #dc2626);
80
+ font-weight: 600;
81
+ margin-inline-start: 0.125rem;
82
+ }
83
+
84
+ @media (forced-colors: active) {
85
+ .checkbox-input {
86
+ forced-color-adjust: auto;
87
+ }
88
+ }
89
+
90
+ @container (max-width: 400px) {
91
+ div:has(> input[type=checkbox]) {
92
+ flex-direction: column;
93
+ align-items: flex-start;
94
+ }
95
+ }
1
96
  :root {
2
97
  --input-border-color: gray;
3
98
  --input-appearance: none;
@@ -19,6 +114,22 @@
19
114
  --placeholder-fs: smaller;
20
115
  --form-direction: column;
21
116
  --select-arrow: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20'><polyline points='6,9 10,13 14,9' stroke='%23000000' stroke-width='1.5' fill='none' /></svg>");
117
+ /* ==========================================================================
118
+ Size Tokens
119
+ ========================================================================== */
120
+ --checkbox-size-sm: 1rem; /* 16px */
121
+ --checkbox-size-md: 1.25rem; /* 20px */
122
+ --checkbox-size-lg: 1.5rem; /* 24px */
123
+ /* ==========================================================================
124
+ Base Properties
125
+ ========================================================================== */
126
+ --checkbox-size: var(--checkbox-size-md);
127
+ --checkbox-bg: #ffffff;
128
+ --checkbox-border: 0.125rem solid #6b7280; /* 2px border */
129
+ --checkbox-border-color: #6b7280; /* Gray 500 */
130
+ --checkbox-radius: 0.25rem; /* 4px */
131
+ --checkbox-cursor: pointer;
132
+ --checkbox-transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
22
133
  }
23
134
 
24
135
  form {
@@ -35,9 +146,7 @@ form label {
35
146
  display: block;
36
147
  }
37
148
 
38
- input[type]:not([type=checkbox], [type=radio]),
39
- textarea,
40
- select {
149
+ input {
41
150
  -webkit-appearance: var(--input-appearance);
42
151
  -moz-appearance: var(--input-appearance);
43
152
  appearance: var(--input-appearance);
@@ -49,6 +158,19 @@ select {
49
158
  border-radius: var(--input-radius);
50
159
  background-color: var(--input-bg, #fff);
51
160
  }
161
+ input:focus-visible, input:focus {
162
+ outline: var(--input-focus-outline);
163
+ outline-offset: var(--input-focus-outline-offset);
164
+ }
165
+ input[aria-disabled=true], input:disabled {
166
+ --input-border-color: lightgray;
167
+ background-color: var(--input-disabled-bg);
168
+ opacity: var(--input-disabled-opacity);
169
+ cursor: var(--input-disabled-cursor);
170
+ text-transform: capitalize;
171
+ text-decoration: line-through;
172
+ }
173
+
52
174
  input[type]:not([type=checkbox], [type=radio])::placeholder,
53
175
  textarea::placeholder,
54
176
  select::placeholder {
@@ -57,14 +179,6 @@ select::placeholder {
57
179
  font-size: var(--placeholder-fs);
58
180
  text-transform: capitalize;
59
181
  }
60
- input[type]:not([type=checkbox], [type=radio]):focus-visible, input[type]:not([type=checkbox], [type=radio]):focus,
61
- textarea:focus-visible,
62
- textarea:focus,
63
- select:focus-visible,
64
- select:focus {
65
- outline: var(--input-focus-outline);
66
- outline-offset: var(--input-focus-outline-offset);
67
- }
68
182
  input[type]:not([type=checkbox], [type=radio])[aria-required=true]::placeholder,
69
183
  textarea[aria-required=true]::placeholder,
70
184
  select[aria-required=true]::placeholder {
@@ -76,17 +190,19 @@ textarea[aria-required=true]::placeholder::after,
76
190
  select[aria-required=true]::placeholder::after {
77
191
  content: "* ";
78
192
  }
79
- input[type]:not([type=checkbox], [type=radio])[aria-disabled=true], input[type]:not([type=checkbox], [type=radio]):disabled,
80
- textarea[aria-disabled=true],
81
- textarea:disabled,
82
- select[aria-disabled=true],
83
- select:disabled {
84
- --input-border-color: lightgray;
85
- background-color: var(--input-disabled-bg);
86
- opacity: var(--input-disabled-opacity);
87
- cursor: var(--input-disabled-cursor);
88
- text-transform: capitalize;
89
- text-decoration: line-through;
193
+
194
+ input[type=checkbox] {
195
+ opacity: 1;
196
+ width: var(--checkbox-size);
197
+ height: var(--checkbox-size);
198
+ margin: 0;
199
+ cursor: var(--checkbox-cursor);
200
+ flex-shrink: 0;
201
+ }
202
+ input[type=checkbox]:checked {
203
+ background-color: var(--checkbox-bg, red);
204
+ outline: rebeccapurple solid 2px;
205
+ background: #dbeafe;
90
206
  }
91
207
 
92
208
  select {
@@ -1 +1 @@
1
- {"version":3,"sourceRoot":"","sources":["../../components/form/form.scss"],"names":[],"mappings":"AAAA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;EAGA;EACA;EACA;EAGA;EACA;EACA;EAEA;EACA;;;AAGF;EACE;EACA;EACA;;AACA;EACE;EACA;EACA;;AAGF;EACE;;;AAIJ;AAAA;AAAA;EAGE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;AAAA;AAAA;EACE;EACA;EACA;EACA;;AAGF;AAAA;AAAA;AAAA;AAAA;EACE;EACA;;AAKA;AAAA;AAAA;EACE;EACA;;AACA;AAAA;AAAA;EACE;;AAKN;AAAA;AAAA;AAAA;AAAA;EAEE;EACA;EACA;EACA;EACA;EACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA","file":"form.css"}
1
+ {"version":3,"sourceRoot":"","sources":["../../components/form/checkbox.scss","../../components/form/form.scss"],"names":[],"mappings":"AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA+BA;EACE;EACA;EACA;EACA;;AAGA;EACE;EACA;;AAKA;EACE;;AAOF;EACE;EAEA;EACA;;AAMJ;EACE;EACA;;AAEA;EACE;EACA;;AAOF;EACE;;AAMF;EACE;;;AAMN;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGA;EAXF;IAYI;;;AAGF;EACE;EACA;EACA;;;AAQF;EAHF;IAII;;;;AAMJ;EACE;IACE;IACA;;;AC3HJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;EAGA;EACA;EACA;EAGA;EACA;EACA;EAEA;EACA;AAEA;AAAA;AAAA;EAIA;EACA;EACA;AAEA;AAAA;AAAA;EAIA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;AACA;EACE;EACA;EACA;;AAGF;EACE;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EAEE;EACA;;AAGF;EAEE;EACA;EACA;EACA;EACA;EACA;;;AAOF;AAAA;AAAA;EACE;EACA;EACA;EACA;;AAGA;AAAA;AAAA;EACE;EACA;;AACA;AAAA;AAAA;EACE;;;AAMR;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA","file":"form.css"}