picocrank 1.23.0 → 1.24.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.
package/AGENTS.md ADDED
@@ -0,0 +1,11 @@
1
+ # PicoCrank — agent notes
2
+
3
+ PicoCrank is a Vue component library demo built on the [Femtocrank](https://github.com/jamesread/femtocrank) global CSS theme. Reusable components live under `vue/components/`; example pages under `vue/examples/`.
4
+
5
+ ## UI patterns
6
+
7
+ - [Buttons with icons](docs/buttons-with-icons.md) — Femtocrank `.inline-icon`, Hugeicons, karma variants (icon + text and icon-only).
8
+
9
+ ## Examples app
10
+
11
+ Run the Vite dev server and browse example routes (e.g. `/buttons-example`) for live reference implementations.
@@ -0,0 +1,97 @@
1
+ # Buttons with icons (PicoCrank / Femtocrank)
2
+
3
+ Brief reference for reproducing icon buttons as shown in `vue/examples/ButtonsExample.vue`.
4
+
5
+ ## Prerequisites
6
+
7
+ - **Femtocrank** `^2.5.3` or newer (provides `.inline-icon` and button/karma styles).
8
+ - **Hugeicons**: `@hugeicons/vue` and `@hugeicons/core-free-icons`.
9
+ - There is **no** `Button` Vue component — use plain HTML `<button>` (or `<a class="button">` for links).
10
+
11
+ ## Pattern
12
+
13
+ 1. Put **`inline-icon`** on the `<button>` (Femtocrank flex layout: icon + label, aligned with gap).
14
+ 2. Add optional **karma intent** classes on the same element: `neutral`, `good`, `bad`, `warning`.
15
+ 3. Render **`HugeiconsIcon`** at `1em` × `1em` with **`strokeWidth` `2.5`** for button-sized icons.
16
+ 4. Set **`aria-hidden="true"`** on the icon when visible text is present.
17
+ 5. For **icon-only** buttons, omit the label and set **`aria-label`** on the button instead.
18
+
19
+ ## Icon and text
20
+
21
+ ```vue
22
+ <script setup>
23
+ import { HugeiconsIcon } from '@hugeicons/vue'
24
+ import { Add01Icon } from '@hugeicons/core-free-icons'
25
+
26
+ const iconStrokeWidth = 2.5
27
+ </script>
28
+
29
+ <template>
30
+ <button type="button" class="inline-icon good">
31
+ <HugeiconsIcon
32
+ :icon="Add01Icon"
33
+ width="1em"
34
+ height="1em"
35
+ :strokeWidth="iconStrokeWidth"
36
+ aria-hidden="true"
37
+ />
38
+ <span>Add item</span>
39
+ </button>
40
+ </template>
41
+ ```
42
+
43
+ ## Icon only
44
+
45
+ ```vue
46
+ <button type="button" class="inline-icon neutral" aria-label="Edit">
47
+ <HugeiconsIcon
48
+ :icon="EditIcon"
49
+ width="1em"
50
+ height="1em"
51
+ :strokeWidth="iconStrokeWidth"
52
+ aria-hidden="true"
53
+ />
54
+ </button>
55
+ ```
56
+
57
+ ## Karma variants
58
+
59
+ Apply on the same `<button>` as `inline-icon`:
60
+
61
+ | Class | Use for |
62
+ |-----------|----------------------|
63
+ | (none) | Default |
64
+ | `neutral` | Secondary / toolbar |
65
+ | `good` | Primary / confirm |
66
+ | `bad` | Destructive |
67
+ | `warning` | Caution |
68
+
69
+ Example: `<button type="button" class="inline-icon bad" aria-label="Delete">…</button>`
70
+
71
+ ## Toolbars
72
+
73
+ Group buttons in a flex row (example-only layout, not from Femtocrank):
74
+
75
+ ```vue
76
+ <div role="toolbar" class="buttons-row">
77
+ <!-- buttons here -->
78
+ </div>
79
+ ```
80
+
81
+ ```css
82
+ .buttons-row {
83
+ display: flex;
84
+ flex-wrap: wrap;
85
+ gap: 0.5rem;
86
+ }
87
+ ```
88
+
89
+ ## Do not
90
+
91
+ - Do not create a wrapper `Button` component unless the project already has one.
92
+ - Do not omit `aria-label` on icon-only buttons.
93
+ - Do not set `aria-hidden` on the button itself — only on the icon when a visible label exists.
94
+
95
+ ## Live example
96
+
97
+ Route: `/buttons-example` — sections **Icon and text** and **Icon only** in `vue/examples/ButtonsExample.vue`.
package/index.html CHANGED
@@ -24,7 +24,7 @@
24
24
 
25
25
  <script type = "module">
26
26
  import { createApp } from 'vue';
27
- import App from './vue/views/App.vue';
27
+ import App from './vue/examples/App.vue';
28
28
  import router from './router.js';
29
29
 
30
30
  const app = createApp(App)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "picocrank",
3
- "version": "1.23.0",
3
+ "version": "1.24.0",
4
4
  "description": "A set of zero dependency Vue components built on the Femtocrank global CSS theme.",
5
5
  "main": "vite.config.js",
6
6
  "scripts": {
@@ -21,7 +21,7 @@
21
21
  "@hugeicons/core-free-icons": "^4.2.3",
22
22
  "@hugeicons/vue": "^1.0.7",
23
23
  "@vitejs/plugin-vue": "^6.0.8",
24
- "femtocrank": "^2.5.2",
24
+ "femtocrank": "^2.5.3",
25
25
  "unplugin-vue-components": "^32.1.0",
26
26
  "vite": "^8.2.1",
27
27
  "vue": "^3.5.41",
package/router.js CHANGED
@@ -20,7 +20,7 @@ const routes = [
20
20
  {
21
21
  name: 'Welcome',
22
22
  path: '/',
23
- component: () => import('./vue/views/Welcome.vue'),
23
+ component: () => import('./vue/examples/Welcome.vue'),
24
24
  meta: {
25
25
  title: 'Welcome',
26
26
  icon: HomeIcon,
@@ -29,7 +29,7 @@ const routes = [
29
29
  {
30
30
  name: 'PageStructure',
31
31
  path: '/page-structure',
32
- component: () => import('./vue/views/PageStructure.vue'),
32
+ component: () => import('./vue/examples/PageStructure.vue'),
33
33
  meta: {
34
34
  title: 'Page structure',
35
35
  icon: LayoutGridIcon,
@@ -38,7 +38,7 @@ const routes = [
38
38
  {
39
39
  name: 'ThemePreview',
40
40
  path: '/theme-preview',
41
- component: () => import('./vue/views/ThemePreview.vue'),
41
+ component: () => import('./vue/examples/ThemePreview.vue'),
42
42
  meta: {
43
43
  title: 'Theme Switcher',
44
44
  icon: PaintBoardIcon,
@@ -47,7 +47,7 @@ const routes = [
47
47
  {
48
48
  name: 'UserDetails',
49
49
  path: '/user',
50
- component: () => import('./vue/views/UserDetails.vue'),
50
+ component: () => import('./vue/examples/UserDetails.vue'),
51
51
  meta: {
52
52
  title: 'User details',
53
53
  icon: UserIcon,
@@ -57,7 +57,7 @@ const routes = [
57
57
  name: 'TableExample',
58
58
  path: '/table-example',
59
59
  title: 'Table Example',
60
- component: () => import('./vue/views/TableExample.vue'),
60
+ component: () => import('./vue/examples/TableExample.vue'),
61
61
  meta: {
62
62
  title: 'Table',
63
63
  icon: TableIcon,
@@ -66,7 +66,7 @@ const routes = [
66
66
  {
67
67
  name: 'TableRemoteExample',
68
68
  path: '/table-remote-example',
69
- component: () => import('./vue/views/TableRemoteExample.vue'),
69
+ component: () => import('./vue/examples/TableRemoteExample.vue'),
70
70
  meta: {
71
71
  title: 'Remote table',
72
72
  icon: TableIcon,
@@ -75,7 +75,7 @@ const routes = [
75
75
  {
76
76
  name: 'ViewItem',
77
77
  path: '/view-item/:id',
78
- component: () => import('./vue/views/ViewItem.vue'),
78
+ component: () => import('./vue/examples/ViewItem.vue'),
79
79
  props: true,
80
80
  meta: {
81
81
  title: 'View item',
@@ -85,7 +85,7 @@ const routes = [
85
85
  {
86
86
  name: 'Admin',
87
87
  path: '/admin',
88
- component: () => import('./vue/views/Admin.vue'),
88
+ component: () => import('./vue/examples/Admin.vue'),
89
89
  meta: {
90
90
  title: 'Admin',
91
91
  icon: SecurityValidationIcon,
@@ -94,7 +94,7 @@ const routes = [
94
94
  {
95
95
  name: 'CalendarExample',
96
96
  path: '/calendar-example',
97
- component: () => import('./vue/views/CalendarExample.vue'),
97
+ component: () => import('./vue/examples/CalendarExample.vue'),
98
98
  props: true,
99
99
  meta: {
100
100
  title: 'Calendar',
@@ -104,7 +104,7 @@ const routes = [
104
104
  {
105
105
  name: 'Login',
106
106
  path: '/login',
107
- component: () => import('./vue/views/Login.vue'),
107
+ component: () => import('./vue/examples/Login.vue'),
108
108
  meta: {
109
109
  title: 'Login',
110
110
  icon: SecurityValidationIcon,
@@ -113,7 +113,7 @@ const routes = [
113
113
  {
114
114
  name: 'ButtonsExample',
115
115
  path: '/buttons-example',
116
- component: () => import('./vue/views/ButtonsExample.vue'),
116
+ component: () => import('./vue/examples/ButtonsExample.vue'),
117
117
  meta: {
118
118
  title: 'Buttons',
119
119
  icon: CursorPointer01Icon,
@@ -122,7 +122,7 @@ const routes = [
122
122
  {
123
123
  name: 'FormExample',
124
124
  path: '/form-example',
125
- component: () => import('./vue/views/FormExample.vue'),
125
+ component: () => import('./vue/examples/FormExample.vue'),
126
126
  meta: {
127
127
  title: 'Forms',
128
128
  icon: EditIcon,
@@ -131,7 +131,7 @@ const routes = [
131
131
  {
132
132
  name: 'ReadOnlyTextAreaExample',
133
133
  path: '/readonly-textarea-example',
134
- component: () => import('./vue/views/ReadOnlyTextAreaExample.vue'),
134
+ component: () => import('./vue/examples/ReadOnlyTextAreaExample.vue'),
135
135
  meta: {
136
136
  title: 'Read-only output',
137
137
  icon: ClipboardCopyIcon,
@@ -140,7 +140,7 @@ const routes = [
140
140
  {
141
141
  name: 'StatusExample',
142
142
  path: '/status-example',
143
- component: () => import('./vue/views/StatusExample.vue'),
143
+ component: () => import('./vue/examples/StatusExample.vue'),
144
144
  meta: {
145
145
  title: 'Status & notifications',
146
146
  icon: StatusIcon,
@@ -149,7 +149,7 @@ const routes = [
149
149
  {
150
150
  name: 'DialogExample',
151
151
  path: '/dialog-example',
152
- component: () => import('./vue/views/DialogExample.vue'),
152
+ component: () => import('./vue/examples/DialogExample.vue'),
153
153
  meta: {
154
154
  title: 'Dialog',
155
155
  icon: InformationCircleIcon,
@@ -158,7 +158,7 @@ const routes = [
158
158
  {
159
159
  name: 'TabsExample',
160
160
  path: '/tabs-example',
161
- component: () => import('./vue/views/TabsExample.vue'),
161
+ component: () => import('./vue/examples/TabsExample.vue'),
162
162
  meta: {
163
163
  title: 'Tabs',
164
164
  icon: Menu01Icon,
@@ -167,7 +167,7 @@ const routes = [
167
167
  {
168
168
  name: 'NavigationGridExample',
169
169
  path: '/navigation-grid',
170
- component: () => import('./vue/views/NavigationGridExample.vue'),
170
+ component: () => import('./vue/examples/NavigationGridExample.vue'),
171
171
  meta: {
172
172
  title: 'Navigation Grid',
173
173
  icon: Navigation01Icon,
@@ -176,7 +176,7 @@ const routes = [
176
176
  {
177
177
  name: 'LoginExample',
178
178
  path: '/login-example',
179
- component: () => import('./vue/views/LoginExample.vue'),
179
+ component: () => import('./vue/examples/LoginExample.vue'),
180
180
  meta: {
181
181
  title: 'Login',
182
182
  icon: SecurityValidationIcon,
package/vite.config.js CHANGED
@@ -94,7 +94,7 @@ export default defineConfig({
94
94
  plugins: [
95
95
  themesIndexPlugin(),
96
96
  Components({
97
- dirs: "./vue/",
97
+ dirs: ['./vue/components/'],
98
98
  extensions: ['vue'],
99
99
  deep: true,
100
100
  dts: false,
@@ -1,7 +1,35 @@
1
1
  <template>
2
2
  <label v-if="!fake" :for="controlId" :class="{ disabled }">{{ label }}</label>
3
3
  <span v-else class="fake-label" :class="{ disabled }">{{ label }}</span>
4
- <slot />
4
+ <div v-if="hasFieldMeta" class="form-field-control">
5
+ <template v-if="descriptionAbove">
6
+ <div v-if="description || docsUrl" class="form-field-meta">
7
+ <p v-if="description" class="subtle form-field-description">{{ description }}</p>
8
+ <a
9
+ v-if="docsUrl"
10
+ class="form-field-docs-link"
11
+ :href="docsUrl"
12
+ target="_blank"
13
+ rel="noopener noreferrer"
14
+ >{{ docsLinkTitle }}</a>
15
+ </div>
16
+ <slot />
17
+ </template>
18
+ <template v-else>
19
+ <slot />
20
+ <div v-if="description || docsUrl" class="form-field-meta">
21
+ <p v-if="description" class="subtle form-field-description">{{ description }}</p>
22
+ <a
23
+ v-if="docsUrl"
24
+ class="form-field-docs-link"
25
+ :href="docsUrl"
26
+ target="_blank"
27
+ rel="noopener noreferrer"
28
+ >{{ docsLinkTitle }}</a>
29
+ </div>
30
+ </template>
31
+ </div>
32
+ <slot v-else />
5
33
  </template>
6
34
 
7
35
  <script setup>
@@ -12,6 +40,22 @@ const props = defineProps({
12
40
  type: String,
13
41
  required: true,
14
42
  },
43
+ description: {
44
+ type: String,
45
+ default: '',
46
+ },
47
+ descriptionAbove: {
48
+ type: Boolean,
49
+ default: false,
50
+ },
51
+ docsUrl: {
52
+ type: String,
53
+ default: '',
54
+ },
55
+ docsUrlTitle: {
56
+ type: String,
57
+ default: '',
58
+ },
15
59
  for: {
16
60
  type: String,
17
61
  default: '',
@@ -31,6 +75,8 @@ const props = defineProps({
31
75
  })
32
76
 
33
77
  const controlId = computed(() => props.htmlFor || props.for)
78
+ const hasFieldMeta = computed(() => Boolean(props.description || props.docsUrl))
79
+ const docsLinkTitle = computed(() => props.docsUrlTitle || props.docsUrl)
34
80
  </script>
35
81
 
36
82
  <style scoped>
@@ -39,4 +85,26 @@ label.disabled,
39
85
  color: var(--disabled-text-color);
40
86
  cursor: not-allowed;
41
87
  }
88
+
89
+ .form-field-control {
90
+ display: flex;
91
+ flex-direction: column;
92
+ gap: 0.5rem;
93
+ width: 100%;
94
+ }
95
+
96
+ .form-field-meta {
97
+ display: flex;
98
+ flex-direction: column;
99
+ gap: 0.35rem;
100
+ }
101
+
102
+ .form-field-description {
103
+ margin: 0;
104
+ }
105
+
106
+ .form-field-docs-link {
107
+ font-size: 0.9em;
108
+ align-self: flex-start;
109
+ }
42
110
  </style>
@@ -5,7 +5,9 @@
5
5
  >
6
6
  <p>
7
7
  Buttons use the global Femtocrank theme. Apply karma classes for intent, or
8
- rely on <code>type</code> attributes for form semantics.
8
+ rely on <code>type</code> attributes for form semantics. Use
9
+ <code>.inline-icon</code> on buttons with icons, and
10
+ <code>aria-label</code> when there is no visible text.
9
11
  </p>
10
12
  </Section>
11
13
 
@@ -19,6 +21,47 @@
19
21
  </div>
20
22
  </Section>
21
23
 
24
+ <Section title="Icon and text" subtitle="Hugeicons with a visible label">
25
+ <div role="toolbar" class="buttons-row">
26
+ <button type="button" class="inline-icon">
27
+ <HugeiconsIcon :icon="Add01Icon" width="1em" height="1em" :strokeWidth="iconStrokeWidth" aria-hidden="true" />
28
+ <span>Add item</span>
29
+ </button>
30
+ <button type="button" class="inline-icon neutral">
31
+ <HugeiconsIcon :icon="EditIcon" width="1em" height="1em" :strokeWidth="iconStrokeWidth" aria-hidden="true" />
32
+ <span>Edit</span>
33
+ </button>
34
+ <button type="button" class="inline-icon good">
35
+ <HugeiconsIcon :icon="SearchIcon" width="1em" height="1em" :strokeWidth="iconStrokeWidth" aria-hidden="true" />
36
+ <span>Search</span>
37
+ </button>
38
+ <button type="button" class="inline-icon">
39
+ <HugeiconsIcon :icon="Copy01Icon" width="1em" height="1em" :strokeWidth="iconStrokeWidth" aria-hidden="true" />
40
+ <span>Copy</span>
41
+ </button>
42
+ </div>
43
+ </Section>
44
+
45
+ <Section title="Icon only" subtitle="Hugeicons without visible text">
46
+ <div role="toolbar" class="buttons-row">
47
+ <button type="button" class="inline-icon" aria-label="Add item">
48
+ <HugeiconsIcon :icon="Add01Icon" width="1em" height="1em" :strokeWidth="iconStrokeWidth" aria-hidden="true" />
49
+ </button>
50
+ <button type="button" class="inline-icon neutral" aria-label="Edit">
51
+ <HugeiconsIcon :icon="EditIcon" width="1em" height="1em" :strokeWidth="iconStrokeWidth" aria-hidden="true" />
52
+ </button>
53
+ <button type="button" class="inline-icon good" aria-label="Search">
54
+ <HugeiconsIcon :icon="SearchIcon" width="1em" height="1em" :strokeWidth="iconStrokeWidth" aria-hidden="true" />
55
+ </button>
56
+ <button type="button" class="inline-icon bad" aria-label="Delete">
57
+ <HugeiconsIcon :icon="Delete02Icon" width="1em" height="1em" :strokeWidth="iconStrokeWidth" aria-hidden="true" />
58
+ </button>
59
+ <button type="button" class="inline-icon neutral" aria-label="Settings">
60
+ <HugeiconsIcon :icon="Settings01Icon" width="1em" height="1em" :strokeWidth="iconStrokeWidth" aria-hidden="true" />
61
+ </button>
62
+ </div>
63
+ </Section>
64
+
22
65
  <Section title="Form actions" subtitle="Semantic button types in a fieldset">
23
66
  <FormLayout class="buttons-demo-form" @submit.prevent="onSubmit">
24
67
  <template #actions>
@@ -67,10 +110,20 @@
67
110
 
68
111
  <script setup>
69
112
  import { ref } from 'vue'
113
+ import { HugeiconsIcon } from '@hugeicons/vue'
114
+ import {
115
+ Add01Icon,
116
+ Copy01Icon,
117
+ Delete02Icon,
118
+ EditIcon,
119
+ SearchIcon,
120
+ Settings01Icon,
121
+ } from '@hugeicons/core-free-icons'
70
122
  import Section from '../components/Section.vue'
71
123
  import FormLayout from '../components/FormLayout.vue'
72
124
 
73
125
  const lastAction = ref('')
126
+ const iconStrokeWidth = 2.5
74
127
 
75
128
  function onSubmit() {
76
129
  lastAction.value = 'Form submitted.'
@@ -8,7 +8,11 @@
8
8
  </template>
9
9
 
10
10
  <FormLayout @submit.prevent="handleSubmit">
11
- <FormField label="Name" for="name">
11
+ <FormField
12
+ label="Name"
13
+ for="name"
14
+ description="Your full name as it will appear on your profile."
15
+ >
12
16
  <input
13
17
  id="name"
14
18
  v-model="formData.name"
@@ -46,7 +50,14 @@
46
50
  </select>
47
51
  </FormField>
48
52
 
49
- <FormField label="Compensation" fake>
53
+ <FormField
54
+ label="Compensation"
55
+ fake
56
+ description-above
57
+ description="Select the incentive plan that applies to this role."
58
+ docs-url="https://github.com/jamesread/femtocrank"
59
+ docs-url-title="Femtocrank documentation"
60
+ >
50
61
  <RadioGroup
51
62
  v-model="formData.compensation"
52
63
  name="compensation"
@@ -55,7 +66,14 @@
55
66
  />
56
67
  </FormField>
57
68
 
58
- <FormField label="Accessibility requirements" fake>
69
+ <FormField
70
+ label="Accessibility requirements"
71
+ fake
72
+ description-above
73
+ description="Choose any accommodations needed for this role."
74
+ docs-url="https://www.w3.org/WAI/fundamentals/accessibility-intro/"
75
+ docs-url-title="Web accessibility introduction"
76
+ >
59
77
  <CheckGroup
60
78
  v-model="formData.accessibility"
61
79
  name="accessibility"
@@ -63,7 +81,13 @@
63
81
  />
64
82
  </FormField>
65
83
 
66
- <FormField label="Email" for="email">
84
+ <FormField
85
+ label="Email"
86
+ for="email"
87
+ description="We'll use this address for account notifications only."
88
+ docs-url="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email"
89
+ docs-url-title="HTML email input reference"
90
+ >
67
91
  <input
68
92
  id="email"
69
93
  v-model="formData.email"
@@ -81,7 +105,13 @@
81
105
  />
82
106
  </FormField>
83
107
 
84
- <FormField label="Website" for="website">
108
+ <FormField
109
+ label="Website"
110
+ for="website"
111
+ description="Include https:// for external links."
112
+ docs-url="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/url"
113
+ docs-url-title="HTML URL input reference"
114
+ >
85
115
  <input
86
116
  id="website"
87
117
  v-model="formData.website"
@@ -141,7 +171,11 @@
141
171
  </div>
142
172
  </FormField>
143
173
 
144
- <FormField label="Comments" for="comments">
174
+ <FormField
175
+ label="Comments"
176
+ for="comments"
177
+ description="Optional feedback or notes about this submission."
178
+ >
145
179
  <textarea
146
180
  id="comments"
147
181
  v-model="formData.comments"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes