@stamcat/craftsman 0.0.27-alpha.2 → 0.0.27-alpha.21
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.
- package/AGENTS.md +19 -344
- package/Components.esm.js +72 -11
- package/README.md +5 -0
- package/Styles.esm.js +7 -7
- package/Utilities.esm.js +2 -2
- package/_virtual/_rolldown/runtime.esm.js +18 -6
- package/package.json +75 -6
- package/src/components/Button/AGENTS.md +65 -0
- package/src/components/{Button.d.ts → Button/Button.d.ts} +1 -1
- package/src/components/Button/Button.esm.js +20 -0
- package/src/components/Carousel/AGENTS.md +41 -0
- package/src/components/Carousel/Carousel.css +1 -0
- package/src/components/Carousel/Carousel.d.ts +11 -0
- package/src/components/Carousel/Carousel.scss +34 -0
- package/src/components/Carousel/Carousel2.esm.js +75 -0
- package/src/components/Checkbox/AGENTS.md +34 -0
- package/src/components/Checkbox/Checkbox.d.ts +8 -0
- package/src/components/Checkbox/Checkbox.esm.js +18 -0
- package/src/components/DatePicker/AGENTS.md +36 -0
- package/src/components/DatePicker/DatePicker.css +1 -0
- package/src/components/DatePicker/DatePicker.d.ts +8 -0
- package/src/components/DatePicker/DatePicker.scss +61 -0
- package/src/components/DatePicker/DatePicker2.esm.js +29 -0
- package/src/components/DatePicker/ReactCalendar.css +1 -0
- package/src/components/DatePicker/ReactCalendar.scss +158 -0
- package/src/components/DatePicker/_DatePicker.scss +73 -0
- package/src/components/DateRangePicker/AGENTS.md +38 -0
- package/src/components/DateRangePicker/DateRangePicker.css +1 -0
- package/src/components/DateRangePicker/DateRangePicker.d.ts +8 -0
- package/src/components/DateRangePicker/DateRangePicker.scss +62 -0
- package/src/components/DateRangePicker/DateRangePicker2.esm.js +29 -0
- package/src/components/DateTimePicker/AGENTS.md +41 -0
- package/src/components/DateTimePicker/DateTimePicker.css +1 -0
- package/src/components/DateTimePicker/DateTimePicker.d.ts +13 -0
- package/src/components/DateTimePicker/DateTimePicker.scss +86 -0
- package/src/components/DateTimePicker/DateTimePicker2.esm.js +82 -0
- package/src/components/Icons/IconAmazonPay.d.ts +2 -0
- package/src/components/Icons/IconAmazonPay.esm.js +76 -0
- package/src/components/Icons/IconAmex.d.ts +2 -0
- package/src/components/Icons/IconAmex.esm.js +87 -0
- package/src/components/Icons/IconApplePay.d.ts +2 -0
- package/src/components/Icons/IconApplePay.esm.js +26 -0
- package/src/components/Icons/IconDiscover.d.ts +2 -0
- package/src/components/Icons/IconDiscover.esm.js +129 -0
- package/src/components/Icons/IconGooglePay.d.ts +2 -0
- package/src/components/Icons/IconGooglePay.esm.js +64 -0
- package/src/components/Icons/IconMaestro.d.ts +2 -0
- package/src/components/Icons/IconMaestro.esm.js +38 -0
- package/src/components/Icons/IconMastercard.d.ts +2 -0
- package/src/components/Icons/IconMastercard.esm.js +27 -0
- package/src/components/Icons/IconPayPal.d.ts +2 -0
- package/src/components/Icons/IconPayPal.esm.js +52 -0
- package/src/components/Icons/IconSepa.d.ts +2 -0
- package/src/components/Icons/IconSepa.esm.js +272 -0
- package/src/components/Icons/IconShopPay.d.ts +2 -0
- package/src/components/Icons/IconShopPay.esm.js +35 -0
- package/src/components/Icons/IconSquare.d.ts +2 -0
- package/src/components/Icons/IconSquare.esm.js +11 -0
- package/src/components/Icons/IconStripe.d.ts +2 -0
- package/src/components/Icons/IconStripe.esm.js +57 -0
- package/src/components/Icons/IconUnionPay.d.ts +2 -0
- package/src/components/Icons/IconUnionPay.esm.js +28 -0
- package/src/components/Icons/IconVenmo.d.ts +2 -0
- package/src/components/Icons/IconVenmo.esm.js +21 -0
- package/src/components/Icons/IconVisa.d.ts +2 -0
- package/src/components/Icons/IconVisa.esm.js +14 -0
- package/src/components/Icons/index.d.ts +15 -0
- package/src/components/Icons/index.esm.js +16 -0
- package/src/components/Icons/types.d.ts +4 -0
- package/src/components/Input/AGENTS.md +30 -0
- package/src/components/Input/Input.css +1 -0
- package/src/components/Input/Input.d.ts +6 -0
- package/src/components/Input/Input.scss +176 -0
- package/src/components/Input/Input2.esm.js +42 -0
- package/src/components/Input/InputWrapper.d.ts +11 -0
- package/src/components/Input/InputWrapper.esm.js +22 -0
- package/src/components/InputNumber/AGENTS.md +37 -0
- package/src/components/InputNumber/InputNumber.css +1 -0
- package/src/components/InputNumber/InputNumber.d.ts +10 -0
- package/src/components/InputNumber/InputNumber.scss +29 -0
- package/src/components/InputNumber/InputNumber2.esm.js +36 -0
- package/src/components/InputPassword/AGENTS.md +29 -0
- package/src/components/{InputPassword.d.ts → InputPassword/InputPassword.d.ts} +2 -2
- package/src/components/InputPassword/InputPassword.esm.js +30 -0
- package/src/components/InputPhone/AGENTS.md +35 -0
- package/src/components/InputPhone/InputPhone.css +1 -0
- package/src/components/InputPhone/InputPhone.d.ts +7 -0
- package/src/components/InputPhone/InputPhone.scss +266 -0
- package/src/components/InputPhone/InputPhone2.esm.js +27 -0
- package/src/components/Loader/AGENTS.md +31 -0
- package/src/components/{Loader.d.ts → Loader/Loader.d.ts} +1 -2
- package/src/components/Loader/Loader.esm.js +19 -0
- package/src/components/Loader/loaders.module.css +1 -0
- package/src/components/Loader/loaders.module.esm.js +21 -0
- package/src/{styles/global/components → components/Loader}/loaders.module.scss +2 -1
- package/src/components/Modal/AGENTS.md +47 -0
- package/src/components/Modal/Modal.css +1 -0
- package/src/components/{Modal.d.ts → Modal/Modal.d.ts} +0 -1
- package/src/components/Modal/Modal.scss +150 -0
- package/src/components/Modal/Modal2.esm.js +53 -0
- package/src/components/Notice/AGENTS.md +42 -0
- package/src/components/Notice/Notice.css +1 -0
- package/src/components/Notice/Notice.d.ts +13 -0
- package/src/components/Notice/Notice.scss +76 -0
- package/src/components/Notice/Notice2.esm.js +56 -0
- package/src/components/Pagination/AGENTS.md +38 -0
- package/src/components/Pagination/Pagination.css +1 -0
- package/src/components/Pagination/Pagination.d.ts +10 -0
- package/src/components/Pagination/Pagination.scss +22 -0
- package/src/components/Pagination/Pagination2.esm.js +64 -0
- package/src/components/RadioButton/AGENTS.md +44 -0
- package/src/components/RadioButton/RadioButton.d.ts +9 -0
- package/src/components/RadioButton/RadioButton.esm.js +18 -0
- package/src/components/Select/AGENTS.md +36 -0
- package/src/components/Select/Select.d.ts +14 -0
- package/src/components/Select/Select.esm.js +26 -0
- package/src/components/SortableList/AGENTS.md +45 -0
- package/src/components/SortableList/ListItem.d.ts +10 -0
- package/src/components/SortableList/ListItem.esm.js +67 -0
- package/src/components/SortableList/SortableList.css +1 -0
- package/src/components/SortableList/SortableList.d.ts +23 -0
- package/src/components/SortableList/SortableList.scss +41 -0
- package/src/components/SortableList/SortableList2.esm.js +54 -0
- package/src/components/SortableList/utilities.d.ts +7 -0
- package/src/components/SortableList/utilities.esm.js +4 -0
- package/src/components/Text/AGENTS.md +32 -0
- package/src/components/Text/Text.css +1 -0
- package/src/components/Text/Text.d.ts +10 -0
- package/src/components/Text/Text.scss +80 -0
- package/src/components/Text/Text2.esm.js +23 -0
- package/src/components/Textarea/AGENTS.md +32 -0
- package/src/components/Textarea/Textarea.d.ts +3 -0
- package/src/components/Textarea/Textarea.esm.js +23 -0
- package/src/components/TimePicker/AGENTS.md +42 -0
- package/src/components/TimePicker/TimePicker.css +1 -0
- package/src/components/TimePicker/TimePicker.d.ts +13 -0
- package/src/components/TimePicker/TimePicker.scss +226 -0
- package/src/components/TimePicker/TimePicker2.esm.js +98 -0
- package/src/components/TimePicker/TimePickerDisplay.d.ts +16 -0
- package/src/components/TimePicker/TimePickerDisplay.esm.js +54 -0
- package/src/components/TimePicker/TimePickerWheel.d.ts +13 -0
- package/src/components/TimePicker/TimePickerWheel.esm.js +61 -0
- package/src/components/TimePicker/constants.d.ts +7 -0
- package/src/components/TimePicker/constants.esm.js +3 -0
- package/src/components/TimePicker/utilities.d.ts +15 -0
- package/src/components/TimePicker/utilities.esm.js +20 -0
- package/src/components/Toggle/AGENTS.md +33 -0
- package/src/components/Toggle/Toggle.css +1 -0
- package/src/components/Toggle/Toggle.d.ts +5 -0
- package/src/components/Toggle/Toggle.scss +64 -0
- package/src/components/Toggle/Toggle2.esm.js +19 -0
- package/src/components/Tooltip/AGENTS.md +41 -0
- package/src/components/Tooltip/Tooltip.css +1 -0
- package/src/components/Tooltip/Tooltip.d.ts +12 -0
- package/src/components/Tooltip/Tooltip.scss +29 -0
- package/src/components/Tooltip/Tooltip2.esm.js +65 -0
- package/src/components/index.d.ts +23 -7
- package/src/stories/assets/Stam.jpg +0 -0
- package/src/stories/assets/kaluah.jpg +0 -0
- package/src/stories/assets/stam2.jpg +0 -0
- package/src/stories/assets/tito.jpg +0 -0
- package/src/stories/documentation/AboutUs.mdx +21 -0
- package/src/stories/documentation/GettingStarted.mdx +117 -0
- package/src/stories/documentation/Introduction.mdx +29 -0
- package/src/stories/documentation/Themes.mdx +264 -0
- package/src/stories/documentation/ToDo.mdx +15 -0
- package/src/stories/molecules/Carousel.scss +21 -0
- package/src/stories/utilities/DeviceDetection.mdx +59 -0
- package/src/styles/_config.scss +30 -0
- package/src/styles/components/ThemeProvider.esm.js +1 -1
- package/src/styles/global/components/_button.scss +56 -52
- package/src/styles/global/components/_checkbox.scss +64 -59
- package/src/styles/global/components/_code.scss +19 -15
- package/src/styles/global/components/_input.scss +29 -37
- package/src/styles/global/components/_radioButton.scss +30 -26
- package/src/styles/global/components/_select.scss +56 -0
- package/src/styles/global/components/_typography.scss +97 -6
- package/src/styles/global/globalStyles.module.scss +1 -129
- package/src/styles/global/globalStyles.scss +87 -1
- package/src/styles/index.d.ts +1 -1
- package/src/styles/theme/components.esm.js +11 -7
- package/src/styles/theme/theme.esm.js +31 -41
- package/src/styles/theme/types.d.ts +4 -1
- package/src/styles/utilities/_functions.scss +117 -0
- package/src/styles/utilities/_mixins.scss +0 -0
- package/src/styles/utilities/_placeholders.scss +6 -0
- package/src/styles/utilities/color.d.ts +1 -1
- package/src/styles/utilities/constants.d.ts +1 -1
- package/src/styles/utilities/layout.d.ts +1 -1
- package/src/styles/utilities/layout.esm.js +3 -3
- package/src/{styles/utilities → utilities}/types.d.ts +61 -3
- package/src/utilities/types.esm.js +85 -0
- package/src/utilities/validations.d.ts +5 -0
- package/src/utilities/validations.esm.js +19 -1
- package/src/components/Button.esm.js +0 -28
- package/src/components/Button.module.css +0 -1
- package/src/components/Button.module.esm.js +0 -9
- package/src/components/Button.module.scss +0 -5
- package/src/components/Checkbox.d.ts +0 -7
- package/src/components/Checkbox.esm.js +0 -11
- package/src/components/Input.d.ts +0 -11
- package/src/components/Input.esm.js +0 -41
- package/src/components/Input.module.css +0 -1
- package/src/components/Input.module.esm.js +0 -11
- package/src/components/Input.module.scss +0 -141
- package/src/components/InputPassword.esm.js +0 -33
- package/src/components/Loader.esm.js +0 -21
- package/src/components/Modal.esm.js +0 -62
- package/src/components/Modal.module.css +0 -1
- package/src/components/Modal.module.esm.js +0 -13
- package/src/components/Modal.module.scss +0 -133
- package/src/components/RadioButton.d.ts +0 -7
- package/src/components/RadioButton.esm.js +0 -11
- package/src/styles/global/components/_mixins.scss +0 -2
- package/src/styles/global/components/button.d.ts +0 -7
- package/src/styles/global/components/loaders.module.css +0 -1
- package/src/styles/global/components/loaders.module.esm.js +0 -21
- package/src/styles/utilities/types.esm.js +0 -54
- package/src/{styles/global/components/loaders.d.ts → components/Loader/types.d.ts} +1 -1
- /package/src/{styles/global/components/loaders.esm.js → components/Loader/types.esm.js} +0 -0
- /package/src/components/{Progress.d.ts → Progress/Progress.d.ts} +0 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Meta } from "@storybook/addon-docs/blocks";
|
|
2
|
+
|
|
3
|
+
<Meta title="Getting Started" />
|
|
4
|
+
|
|
5
|
+
# How To Use `ThemeProvider`
|
|
6
|
+
|
|
7
|
+
This guide shows the RSC-safe styling setup and how to apply:
|
|
8
|
+
|
|
9
|
+
1. custom theme values
|
|
10
|
+
2. base global styles
|
|
11
|
+
|
|
12
|
+
## Styling Pattern
|
|
13
|
+
|
|
14
|
+
- `ThemeProvider`
|
|
15
|
+
- Use in any React app, including Next.js App Router
|
|
16
|
+
- Injects theme CSS variables and component override rules
|
|
17
|
+
- Emits overrides in the `craftsman-theme` CSS layer
|
|
18
|
+
- Renders a `<style>` element and wraps children
|
|
19
|
+
|
|
20
|
+
- Global styles
|
|
21
|
+
- Import once at your application root
|
|
22
|
+
- Emit defaults in the `craftsman-base` CSS layer
|
|
23
|
+
- Do not rely on provider side-effect imports for global styling in RSC setups
|
|
24
|
+
- Base is intentionally lower priority than theme when selector specificity is equal
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
## 1. Create a Shared Theme Object
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import type { Theme } from "@stamcat/craftsman/styles";
|
|
31
|
+
import appRoot from "./app.root.scss?inline";
|
|
32
|
+
import primaryButton from "./primary-button.scss?inline";
|
|
33
|
+
|
|
34
|
+
// Theme config supports raw style objects and raw CSS/Sass strings.
|
|
35
|
+
// You can intermix them within the same theme object.
|
|
36
|
+
export const appTheme: Theme = {
|
|
37
|
+
colors: {
|
|
38
|
+
"--blue500": "#1d4ed8",
|
|
39
|
+
},
|
|
40
|
+
root: appRoot,
|
|
41
|
+
components: {
|
|
42
|
+
button: primaryButton,
|
|
43
|
+
input: {
|
|
44
|
+
borderWidth: "2px",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## 2 (a). Setup In a Standard React App
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import { ThemeProvider } from "@stamcat/craftsman/styles";
|
|
55
|
+
import "@stamcat/craftsman/styles/globalStyles";
|
|
56
|
+
import ReactDOM from "react-dom/client";
|
|
57
|
+
import { Button } from "@stamcat/craftsman/Button";
|
|
58
|
+
import { appTheme } from "./theme";
|
|
59
|
+
|
|
60
|
+
function App() {
|
|
61
|
+
return <Button variant="primary">Save</Button>;
|
|
62
|
+
}
|
|
63
|
+
ReactDOM.createRoot(document.getElementById("root")!).render(
|
|
64
|
+
<ThemeProvider theme={appTheme}>
|
|
65
|
+
<App />
|
|
66
|
+
</ThemeProvider>,
|
|
67
|
+
);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## 2 (b). Setup In Next.js App Router
|
|
71
|
+
|
|
72
|
+
Import global styles once in root layout, then render `ThemeProvider` with your theme:
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
import { ThemeProvider } from "@stamcat/craftsman/styles";
|
|
76
|
+
import "@stamcat/craftsman/styles/globalStyles";
|
|
77
|
+
import { appTheme } from "./theme";
|
|
78
|
+
|
|
79
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
80
|
+
return (
|
|
81
|
+
<html lang="en">
|
|
82
|
+
<body>
|
|
83
|
+
<ThemeProvider theme={appTheme}>{children}</ThemeProvider>
|
|
84
|
+
</body>
|
|
85
|
+
</html>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
RSC note:
|
|
91
|
+
|
|
92
|
+
- `ThemeProvider` emits default style-hoisting metadata for compatibility in React Server Components.
|
|
93
|
+
- If your app renders multiple themed providers, pass a unique `href` per provider instance to avoid hoist key collisions.
|
|
94
|
+
|
|
95
|
+
Selector note:
|
|
96
|
+
|
|
97
|
+
- `theme.components.checkbox` targets `input[type='checkbox']`.
|
|
98
|
+
- `theme.components.radioButton` targets `input[type='radio']`.
|
|
99
|
+
- For HTML-tag component keys such as `button`, `select`, and `textarea`, the override is written to both the tag selector and matching class selector, for example `button, .button`.
|
|
100
|
+
- For non-HTML component keys, the override is written to the component class selector, for example `.modal`.
|
|
101
|
+
|
|
102
|
+
Style declaration note:
|
|
103
|
+
|
|
104
|
+
- `theme.root` accepts either a JS style object or a raw CSS/Sass string.
|
|
105
|
+
- `theme.components.*` accepts either a JS style object or a raw CSS/Sass string for that target selector.
|
|
106
|
+
- For file-based styles, import `*.scss?inline` or `*.css?inline` and pass the resulting string.
|
|
107
|
+
- Use JS style objects for small inline overrides, and SCSS/CSS files when you want larger style blocks or Sass features.
|
|
108
|
+
|
|
109
|
+
- Prefer one provider at app root.
|
|
110
|
+
- Keep `theme` for component and CSS-variable overrides.
|
|
111
|
+
- Use `theme.root` CSS variables to override base design tokens like widths.
|
|
112
|
+
|
|
113
|
+
## Editor Setup (CSS Modules IntelliSense)
|
|
114
|
+
|
|
115
|
+
- Install the recommended VS Code extension: `clinyong.vscode-css-modules`.
|
|
116
|
+
- The project already includes `vite-plugin-typed-css-modules` and `typescript-plugin-css-modules` for typed class names and TS language-service support.
|
|
117
|
+
- After installing extensions or pulling config changes, run `TypeScript: Restart TS Server` from the Command Palette.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Meta } from "@storybook/addon-docs/blocks";
|
|
2
|
+
|
|
3
|
+
<Meta title="Introduction" />
|
|
4
|
+
|
|
5
|
+
# Craftsman Design System Framework
|
|
6
|
+
A lightweight framework to create your own design system for React front-end applications.
|
|
7
|
+
|
|
8
|
+
#### What this isn't...
|
|
9
|
+
This is NOT a design system per-se. A design system comes out of the box fully complete with styles that support your brand. <br />
|
|
10
|
+
You are welcome to use this as-is without providing your own branding, however we believe you will be left wanting more..
|
|
11
|
+
|
|
12
|
+
#### What this is...
|
|
13
|
+
Craftsman is a Framework that requires you to define your own brand styles. The Framework provides a flexible, extensible and scalable solution for reusable components along the way.<br />
|
|
14
|
+
Our goal is to provide industry-standard solutions for global enterprise-level applications, empowering you - the designer or developer - to focus on what matters most: <b>User Experience.</b>
|
|
15
|
+
|
|
16
|
+
## Why not just use `{some_existing_framework}`?
|
|
17
|
+
Many of the existing frameworks out in the wild are far too opinionated. As a result, they become bloated, trying to solve problems you'll never have in ways that contradict everything you product stands for.
|
|
18
|
+
Craftsman strives to be as unopinionated as possible, giving you the freedom to explore solutions that work best for you.<br />
|
|
19
|
+
|
|
20
|
+
When we are forced to assert an opinion, we seek to be as flexible as possible and rely on the open-source community before re-inventing the wheel.
|
|
21
|
+
|
|
22
|
+
## Let's just vibe code...Tokenmaxxing FTW!
|
|
23
|
+
You absolutely can if you want, but we think you'll have a much better starting point with code that is forged by human experience.<br />
|
|
24
|
+
|
|
25
|
+
The Craftsman Framework draws inspiration from <a href="https://en.wikipedia.org/wiki/American_Craftsman" target="_blank">Arts & Crafts movement of the late 19th century</a>
|
|
26
|
+
|
|
27
|
+
> The movement emphasized handwork over mass production. In some ways, it was just as much of a social movement as it was an aesthetic one, emphasizing the plight of the industrial worker and equating moral rectitude with the ability to create beautiful but simple things.
|
|
28
|
+
|
|
29
|
+
In the physical world, you can't throw a nailgun at a pile of wood and have a house, we believe you can't expect different of your coding tools. We use AI intelligently, deliberately, and we limit the scope in such a way that enhances our creativity, rather than use it as a crutch to save us the agony of thought.<br /><br />
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { Meta } from "@storybook/addon-docs/blocks";
|
|
2
|
+
|
|
3
|
+
<Meta title="Themes & Styles" />
|
|
4
|
+
|
|
5
|
+
# Themes & Styles
|
|
6
|
+
|
|
7
|
+
Craftsman themes are plain objects that conform to the `Theme` type. You pass them into `ThemeProvider` and the framework applies them globally via CSS variables and component selectors.
|
|
8
|
+
|
|
9
|
+
Theme cascade order is layered:
|
|
10
|
+
|
|
11
|
+
- Base defaults are emitted in the `craftsman-base` layer.
|
|
12
|
+
- Theme overrides are emitted in the `craftsman-theme` layer.
|
|
13
|
+
- `craftsman-theme` is declared after `craftsman-base`, so theme values win when specificity is equal.
|
|
14
|
+
|
|
15
|
+
Craftsman supports styles in **two different ways** — plain JS objects or raw CSS/Sass strings. They are interchangeable and can be mixed within the same theme object. We encourage you to use HTML5 and take advantage of css nesting wherever possible. When custom styles are required, we intend to make all options available to you, so that you can build the app however works best for your product. Yes, you can even use Failwind.
|
|
16
|
+
|
|
17
|
+
## How CSS Layers Work
|
|
18
|
+
|
|
19
|
+
Craftsman relies on the native [`@layer`](https://developer.mozilla.org/en-US/docs/Web/CSS/@layer) at-rule to control the cascade instead of specificity hacks (`!important`, ID selectors, deeply nested overrides).
|
|
20
|
+
|
|
21
|
+
A `@layer` declares a named cascade layer. Layers are ordered by the position of their *first* declaration, not by where their rules physically live in the file — so declaring the layer order once up front lets every subsequent block, wherever it's authored, slot into that order:
|
|
22
|
+
|
|
23
|
+
```css
|
|
24
|
+
/* declares the order once: craftsman-base loses to craftsman-theme on a tie */
|
|
25
|
+
@layer craftsman-base, craftsman-theme;
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Two rules make this predictable:
|
|
29
|
+
|
|
30
|
+
1. **Unlayered styles always beat layered styles.** Any plain CSS you write with no `@layer` wrapper (your app's own stylesheets, CSS Modules, styled-components, etc.) sits *above* every named layer, regardless of selector specificity. This is what makes Craftsman trivially overridable — you don't need to fight specificity to override a Craftsman default.
|
|
31
|
+
2. **Between layers, declaration order wins on a tie**, not specificity. That's why `craftsman-theme` is declared after `craftsman-base` — a `.button` rule in `craftsman-theme` beats a `.button` rule in `craftsman-base` even though the selectors are equally specific.
|
|
32
|
+
|
|
33
|
+
### Everything Craftsman ships lives in `craftsman-base`
|
|
34
|
+
|
|
35
|
+
Every component stylesheet and the global reset are wrapped in `@layer craftsman-base { ... }`:
|
|
36
|
+
|
|
37
|
+
```scss
|
|
38
|
+
/* src/components/Button/Button.scss (abbreviated) */
|
|
39
|
+
@layer craftsman-base {
|
|
40
|
+
.button {
|
|
41
|
+
border-radius: 6px;
|
|
42
|
+
padding: 0.5em 1em;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
This means all of Craftsman's built-in styling is intentionally low-priority in the cascade:
|
|
48
|
+
|
|
49
|
+
- `ThemeProvider` overrides win automatically because they're emitted in `craftsman-theme`, declared after `craftsman-base`.
|
|
50
|
+
- Your own app CSS wins automatically because it's unlayered, beating both Craftsman layers with zero specificity gymnastics.
|
|
51
|
+
|
|
52
|
+
You never need `!important`, extra class chains, or `:where()` tricks to override a Craftsman component — a plain, unlayered selector of any specificity is enough.
|
|
53
|
+
|
|
54
|
+
## Setup
|
|
55
|
+
|
|
56
|
+
Render `ThemeProvider` once near your app root:
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
import { ThemeProvider } from "@stamcat/craftsman/styles";
|
|
60
|
+
import { myTheme } from "./themes";
|
|
61
|
+
|
|
62
|
+
export function App() {
|
|
63
|
+
return (
|
|
64
|
+
<>
|
|
65
|
+
<ThemeProvider theme={myTheme} />
|
|
66
|
+
{/* your app */}
|
|
67
|
+
</>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The `Theme` type accepts three optional keys:
|
|
73
|
+
|
|
74
|
+
| Key | Purpose |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `colors` | Override any named CSS color variable (e.g. `--blue500`) |
|
|
77
|
+
| `root` | `:root` overrides as an object, serialized style object, or raw CSS/Sass string |
|
|
78
|
+
| `components` | Per-component style overrides keyed by component name |
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 1. Plain JavaScript object
|
|
83
|
+
|
|
84
|
+
The simplest approach. Pass a `CSSObject` directly — identical to what you would write in an inline style object.
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import type { Theme } from "@stamcat/craftsman/styles";
|
|
88
|
+
|
|
89
|
+
export const myTheme: Theme = {
|
|
90
|
+
root: {
|
|
91
|
+
"--w-gutter": "14px",
|
|
92
|
+
},
|
|
93
|
+
components: {
|
|
94
|
+
button: {
|
|
95
|
+
backgroundColor: "#16a34a",
|
|
96
|
+
color: "#ffffff",
|
|
97
|
+
border: "2px solid green",
|
|
98
|
+
borderRadius: "8px",
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Use this when you want a zero-dependency, no-build-step definition that lives entirely in a `.ts` file.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 2. Raw CSS / Sass Strings
|
|
109
|
+
|
|
110
|
+
Pass CSS text directly as strings. For larger themes, import Sass files as compiled inline strings with Vite's `?inline` suffix.
|
|
111
|
+
|
|
112
|
+
```scss
|
|
113
|
+
/* green.root.scss */
|
|
114
|
+
:root {
|
|
115
|
+
--w-gutter: 18px;
|
|
116
|
+
--w-text: 15px;
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```scss
|
|
121
|
+
/* green.button.scss */
|
|
122
|
+
button {
|
|
123
|
+
background-color: #16a34a;
|
|
124
|
+
color: #ffffff;
|
|
125
|
+
border: 2px solid green;
|
|
126
|
+
border-radius: 8px;
|
|
127
|
+
|
|
128
|
+
&.primary {
|
|
129
|
+
background-color: #14532d;
|
|
130
|
+
border-color: #14532d;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import type { Theme } from "@stamcat/craftsman/styles";
|
|
137
|
+
import greenRoot from "./green.root.scss?inline";
|
|
138
|
+
import greenButton from "./green.button.scss?inline";
|
|
139
|
+
|
|
140
|
+
export const myTheme: Theme = {
|
|
141
|
+
colors: {
|
|
142
|
+
"--blue500": "#31198a",
|
|
143
|
+
},
|
|
144
|
+
root: greenRoot,
|
|
145
|
+
components: {
|
|
146
|
+
button: greenButton,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Use this when you want full Sass syntax highlighting, mixins, and file organization while keeping theme injection RSC-safe.
|
|
152
|
+
|
|
153
|
+
### Component selector mapping
|
|
154
|
+
|
|
155
|
+
Theme component entries are written to these selectors:
|
|
156
|
+
|
|
157
|
+
| Component key | Selector target |
|
|
158
|
+
|---|---|
|
|
159
|
+
| `input` | `input:not([type='radio']):not([type='checkbox'])` |
|
|
160
|
+
| `checkbox` | `input[type='checkbox']` |
|
|
161
|
+
| `radioButton` | `input[type='radio']` |
|
|
162
|
+
| HTML-tag keys such as `button`, `select`, `textarea` | both the HTML tag and matching class selector, for example `button, .button` |
|
|
163
|
+
| Non-HTML component keys such as `modal` | component class selector, for example `.modal` |
|
|
164
|
+
|
|
165
|
+
This mapping comes from the exported component name. The theme key is the uncapitalized component name, for example `Button` -> `button`, `RadioButton` -> `radioButton`.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Defining multiple themes
|
|
170
|
+
|
|
171
|
+
Export a record of themes and switch between them at runtime.
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
import type { AppTheme } from "@stamcat/craftsman/styles";
|
|
175
|
+
|
|
176
|
+
export const appThemes: AppTheme = {
|
|
177
|
+
default: { root: { "--w-gutter": "14px" } },
|
|
178
|
+
green: { components: { button: { backgroundColor: "#16a34a" } } },
|
|
179
|
+
brand: { colors: { "--blue500": "#31198a" } },
|
|
180
|
+
};
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
// app.tsx
|
|
185
|
+
import React from "react";
|
|
186
|
+
import ReactDOM from "react-dom/client";
|
|
187
|
+
import { ThemeProvider } from "@stamcat/craftsman/styles";
|
|
188
|
+
import { Button } from "@stamcat/craftsman/Button";
|
|
189
|
+
import { appTheme } from "./theme";
|
|
190
|
+
|
|
191
|
+
function App() {
|
|
192
|
+
return <Button variant="primary">Save</Button>;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
ReactDOM.createRoot(document.getElementById("root")!).render(
|
|
196
|
+
<>
|
|
197
|
+
<ThemeProvider theme={appTheme} />
|
|
198
|
+
<App />
|
|
199
|
+
</>,
|
|
200
|
+
);
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Switch themes using the **Themes** icon in the Storybook toolbar to see each variant applied live across all stories.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Utility Parity (TypeScript + Sass)
|
|
208
|
+
|
|
209
|
+
The `color`, `width`, and `breakpoint` utilities exist in both TypeScript and Sass and follow the same usage patterns.
|
|
210
|
+
|
|
211
|
+
### Color utility
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
import { color } from "@stamcat/craftsman/styles";
|
|
215
|
+
|
|
216
|
+
const border = color("green500");
|
|
217
|
+
const overlay = color("green500", "rgba", 0.24);
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
```scss
|
|
221
|
+
@use "../../styles/utilities/_functions" as u;
|
|
222
|
+
|
|
223
|
+
.example {
|
|
224
|
+
border-color: #{u.color(green500)};
|
|
225
|
+
background-color: #{u.color(green500, rgba, 0.24)};
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Width utility
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
import { width } from "@stamcat/craftsman/styles";
|
|
233
|
+
|
|
234
|
+
const text = width("text");
|
|
235
|
+
const cols = width("column", 3);
|
|
236
|
+
const numeric = width("column", 3, false);
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
```scss
|
|
240
|
+
@use "../../styles/utilities/_functions" as u;
|
|
241
|
+
|
|
242
|
+
.example {
|
|
243
|
+
font-size: #{u.width(text)};
|
|
244
|
+
max-width: #{u.width(column, 3)};
|
|
245
|
+
--column-count: #{u.width(column, 3, false)};
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Breakpoint utility
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
import { breakpoint } from "@stamcat/craftsman/styles";
|
|
253
|
+
|
|
254
|
+
const rules = breakpoint("mobileMax", "h4{font-size:14px;} p{font-size:12.25px;}");
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
```scss
|
|
258
|
+
@use "../../styles/utilities/_functions" as u;
|
|
259
|
+
|
|
260
|
+
@include u.breakpoint(mobileMax) {
|
|
261
|
+
:where(h4) { font-size: var(--w-text, 14px); }
|
|
262
|
+
:where(p) { font-size: calc(var(--w-text, 14px) * 0.875); }
|
|
263
|
+
}
|
|
264
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Meta } from "@storybook/addon-docs/blocks";
|
|
2
|
+
|
|
3
|
+
<Meta title="To Do" />
|
|
4
|
+
|
|
5
|
+
# Global Styles
|
|
6
|
+
- video
|
|
7
|
+
- image
|
|
8
|
+
- radio button alignment to baseline
|
|
9
|
+
- implement dark mode @media (prefers-color-scheme: dark)
|
|
10
|
+
- Accordion
|
|
11
|
+
- Progress
|
|
12
|
+
- Skeleton
|
|
13
|
+
- Toast - enhance to show pile vs stacking
|
|
14
|
+
- Tabs
|
|
15
|
+
- sortable list
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
|
|
2
|
+
@use "../../styles/utilities/_functions" as u;
|
|
3
|
+
|
|
4
|
+
@layer craftsman-base {
|
|
5
|
+
.carouselWrapper {
|
|
6
|
+
width: 400px;
|
|
7
|
+
img {
|
|
8
|
+
width: 100%;
|
|
9
|
+
}
|
|
10
|
+
figcaption {
|
|
11
|
+
position: absolute;
|
|
12
|
+
bottom: 0;
|
|
13
|
+
width: 100%;
|
|
14
|
+
text-align: center;
|
|
15
|
+
font-weight: bold;
|
|
16
|
+
background-color: #{u.color("white", rgba, 0.75)};
|
|
17
|
+
padding: #{u.width("gutter", 0.5)} 0 #{u.width("gutter")};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Meta } from "@storybook/addon-docs/blocks";
|
|
2
|
+
|
|
3
|
+
<Meta title="Utilities/Device Detection" />
|
|
4
|
+
|
|
5
|
+
# Device Detection
|
|
6
|
+
|
|
7
|
+
Craftsman already depends on [`react-device-detect`](https://www.npmjs.com/package/react-device-detect) — it's what powers `TimePicker`'s mobile wheel-picker fallback. **Use it instead of writing custom user-agent sniffing or `window.innerWidth` checks** whenever a decision depends on the actual device, browser, or OS rather than pure viewport width.
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
// DO NOT — fragile, reinvents an already-solved problem
|
|
11
|
+
const isMobile = /Mobi|Android/i.test(navigator.userAgent);
|
|
12
|
+
const isMobile = window.innerWidth < 768;
|
|
13
|
+
|
|
14
|
+
// DO
|
|
15
|
+
import { isMobile } from "react-device-detect";
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Pure layout/visual breakpoints (font size, column count, spacing) still belong in CSS media queries — see the `width()`/`breakpoint()` style utilities. Reach for `react-device-detect` when the branch happens in JS/TS, or the check needs to distinguish device/browser/OS rather than just viewport size.
|
|
19
|
+
|
|
20
|
+
## Boolean Flags
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
import { isMobile, isTablet, isDesktop, isBrowser, isSmartTV, isMobileOnly, isAndroid, isIOS } from "react-device-detect";
|
|
24
|
+
|
|
25
|
+
if (isMobile) {
|
|
26
|
+
// mobile-specific behavior
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`isMobileOnly` excludes tablets — use it instead of `isMobile` whenever tablets need to be treated like desktop/browser.
|
|
31
|
+
|
|
32
|
+
## Declarative View Components
|
|
33
|
+
|
|
34
|
+
As an alternative to branching inside JSX, use the wrapper components — they render `children` only when the matching device is detected:
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { MobileView, BrowserView, TabletView } from "react-device-detect";
|
|
38
|
+
|
|
39
|
+
<MobileView>
|
|
40
|
+
<CompactNav />
|
|
41
|
+
</MobileView>
|
|
42
|
+
<BrowserView>
|
|
43
|
+
<FullNav />
|
|
44
|
+
</BrowserView>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Orientation
|
|
48
|
+
|
|
49
|
+
Use the `useMobileOrientation` hook rather than comparing `window.innerWidth`/`innerHeight` for portrait/landscape checks:
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import { useMobileOrientation } from "react-device-detect";
|
|
53
|
+
|
|
54
|
+
const { isPortrait, isLandscape } = useMobileOrientation();
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Informational Values
|
|
58
|
+
|
|
59
|
+
`browserName`, `osName`, and `deviceType` return strings meant for logging/analytics — prefer the boolean flags above when the result drives conditional logic.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/// Configurable Sass variables for @stamcat/craftsman.
|
|
2
|
+
///
|
|
3
|
+
/// To override breakpoints in your application, load this module first with
|
|
4
|
+
/// your custom values before importing any other craftsman styles:
|
|
5
|
+
///
|
|
6
|
+
/// @example scss
|
|
7
|
+
/// @use "@stamcat/craftsman/styles/config" with (
|
|
8
|
+
/// $bp-tablet: 768,
|
|
9
|
+
/// $bp-desktop: 1200,
|
|
10
|
+
/// $bp-mobileMax: 767.99999,
|
|
11
|
+
/// $bp-tabletMax: 1199.99999,
|
|
12
|
+
/// );
|
|
13
|
+
/// @use "@stamcat/craftsman/styles/global/globalStyles";
|
|
14
|
+
|
|
15
|
+
// All values are unitless numbers (px implied).
|
|
16
|
+
// Default values mirror src/styles/utilities/constants.ts.
|
|
17
|
+
|
|
18
|
+
// Width defaults
|
|
19
|
+
$w-text: 14 !default;
|
|
20
|
+
$w-gutter: 16 !default;
|
|
21
|
+
$w-column: 60 !default;
|
|
22
|
+
|
|
23
|
+
// Breakpoint values used in @media queries.
|
|
24
|
+
// CSS custom properties cannot be used in @media conditions — these must be static values.
|
|
25
|
+
$bp-mobileMax: 659.99999 !default;
|
|
26
|
+
$bp-tablet: 660 !default;
|
|
27
|
+
$bp-tabletMax: 1039.99999 !default;
|
|
28
|
+
$bp-desktop: 1040 !default;
|
|
29
|
+
$bp-desktopMax: 1319.99999 !default;
|
|
30
|
+
$bp-extDesktop: 1320 !default;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { themeBuilder as e } from "../theme/theme.esm.js";
|
|
2
2
|
import { Fragment as t, jsx as n, jsxs as r } from "react/jsx-runtime";
|
|
3
3
|
//#region src/styles/components/ThemeProvider.tsx
|
|
4
|
-
function i({ theme: i, children: a, precedence: o = "
|
|
4
|
+
function i({ theme: i, children: a, precedence: o = "high", href: s = "stamcat-craftsman-theme-provider" }) {
|
|
5
5
|
return /* @__PURE__ */ r(t, { children: [/* @__PURE__ */ n("style", {
|
|
6
6
|
precedence: o,
|
|
7
7
|
href: s,
|
|
@@ -1,64 +1,68 @@
|
|
|
1
|
-
|
|
2
|
-
padding: 0;
|
|
3
|
-
border: none;
|
|
4
|
-
background: none;
|
|
5
|
-
color: var(--blue500);
|
|
1
|
+
@use "../../utilities/functions" as u;
|
|
6
2
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
3
|
+
@layer craftsman-base {
|
|
4
|
+
%text-link {
|
|
5
|
+
padding: 0;
|
|
6
|
+
border: none;
|
|
7
|
+
background: none;
|
|
8
|
+
color: var(--blue500);
|
|
14
9
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
font-weight: 500;
|
|
26
|
-
transition: 0.125s;
|
|
27
|
-
padding: var(--btn-pad-y) var(--btn-pad-x);
|
|
28
|
-
border-radius: var(--btn-border-radius);
|
|
29
|
-
border: 1px solid var(--blue500);
|
|
30
|
-
background-color: var(--white);
|
|
31
|
-
|
|
32
|
-
&:not(:disabled):not(.disabled):hover {
|
|
33
|
-
background-color: var(--teal200);
|
|
10
|
+
&:hover {
|
|
11
|
+
color: var(--teal700);
|
|
12
|
+
background-color: transparent;
|
|
13
|
+
text-decoration: underline;
|
|
14
|
+
cursor: pointer;
|
|
15
|
+
&.disabled, &:disabled {
|
|
16
|
+
cursor: not-allowed;
|
|
17
|
+
color: #{u.color("gray400")};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
34
20
|
}
|
|
35
21
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
22
|
+
%button-styles {
|
|
23
|
+
--btn-size: 1;
|
|
24
|
+
color: var(--blue600);
|
|
25
|
+
border: none;
|
|
26
|
+
cursor: pointer;
|
|
27
|
+
display: inline-flex;
|
|
28
|
+
flex-flow: row wrap;
|
|
29
|
+
align-items: center;
|
|
30
|
+
font-weight: 500;
|
|
31
|
+
transition: 0.125s;
|
|
32
|
+
padding: calc(#{u.width(gutter, 0.5)} * var(--btn-size)) calc(#{u.width(gutter, 0.75)} * var(--btn-size));
|
|
33
|
+
border-radius: calc(#{u.width(gutter, 0.75)} * var(--btn-size));
|
|
34
|
+
font-size: max(10px, calc(#{u.width(text)} * var(--btn-size)));
|
|
35
|
+
border: 1px solid var(--blue500);
|
|
36
|
+
background-color: var(--white);
|
|
37
|
+
|
|
38
|
+
&:hover {
|
|
39
|
+
background-color: var(--teal200);
|
|
40
|
+
}
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
border-color: var(--gray200);
|
|
45
|
-
color: var(--gray400);
|
|
42
|
+
&.primary {
|
|
43
|
+
color: var(--white);
|
|
44
|
+
background-color: var(--blue500);
|
|
46
45
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
&:hover {
|
|
47
|
+
background-color: var(--blue700);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
&.text {
|
|
52
|
+
@extend %text-link;
|
|
52
53
|
}
|
|
53
|
-
}
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
&:disabled,
|
|
56
|
+
&.disabled {
|
|
57
|
+
cursor: not-allowed;
|
|
58
|
+
border-color: #{u.color("gray200")};
|
|
59
|
+
color: #{u.color("gray400")};
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
&.primary {
|
|
62
|
+
border-color: #{u.color("gray200")};
|
|
63
|
+
background-color: #{u.color("gray400")};
|
|
64
|
+
color: #{u.color("gray200")};
|
|
65
|
+
}
|
|
62
66
|
}
|
|
63
67
|
}
|
|
64
68
|
}
|