diff --git a/.agents/skills/bump/SKILL.md b/.agents/skills/bump/SKILL.md new file mode 100644 index 0000000..befbe64 --- /dev/null +++ b/.agents/skills/bump/SKILL.md @@ -0,0 +1,42 @@ +--- +name: 'bump' +description: 'Bump package versions and update CHANGELOG.md so consumers can see what happens between releases. Use when a feature or fix branch is ready.' +--- + +# Changelog & version bump + +`CHANGELOG.md` at the monorepo root tells package consumers what changed. Every branch that changes a published package (`styles`, `react`, `icons`) must bump the affected versions and document the changes before being merged. + +## Workflow + +1. **Identify affected packages**: `git diff origin/main...HEAD --stat` and `git log origin/main..HEAD` show which packages changed. Ignore packages with no consumer-facing change. +2. **Bump versions**: increment the patch number (`0.0.x` + 1) in each affected `/package.json`. The repository convention is one bump per feature branch, folded into the feature commit. +3. **Align internal peer dependencies**: if a package now relies on something introduced in another package of the same branch (e.g. `react` consuming new `styles` CSS classes), update the corresponding `peerDependencies` range (e.g. `"@ippon-ui/styles": "~0.0.7"`). +4. **Reinstall**: run `mise setup` so the lockfile stays consistent. +5. **Update `CHANGELOG.md`**: add a release entry right after the introduction, above the previous entries. +6. **Verify**: `mise build`, `mise lint-ci` and `mise test-unit-ci` must pass. + +## Release entry format + +```markdown +## YYYY-MM-DD — @ippon-ui/styles X.Y.Z · @ippon-ui/react X.Y.Z + +### Added + +- `component` organism: what it brings to the consumer. + +### Changed + +- What changed and what it means for the consumer. +``` + +- The heading lists only the packages bumped by the branch, each with its new version, separated by `·`. +- Use the [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) categories: `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, `Security`. Only include the categories that apply. +- One line per change, in English, consumer-focused: describe what the consumer gets or must adapt, not internal details (CI, tests, tooling stay out unless they affect consumers). +- Wrap component, class, prop and token names in backticks. + +## Conventions + +- A same branch usually produces a single entry, updated as the branch evolves. +- The changelog update and the version bumps belong to the feature commit (branches follow a single squashed commit convention). +- Never rewrite past release entries; a correction is a new entry. diff --git a/.agents/skills/pattern-library/SKILL.md b/.agents/skills/pattern-library/SKILL.md index 6cf29c1..b67ecc2 100644 --- a/.agents/skills/pattern-library/SKILL.md +++ b/.agents/skills/pattern-library/SKILL.md @@ -130,6 +130,37 @@ In HTML: ``` +## Ion + +Beyond CAP, a component can expose an **ion**: a class that starts with `---`, followed by the name of the ion in `kebab-case` (e.g. `ippon-dropdown---buttons`). + +An ion is set on a container and _ionizes_ the descendants that opt in. Unlike a part, the ionized behavior is **not declared by the container**: each affected component declares, in its own SCSS, how it reacts when it is ionized. This inverts the dependency, so a container never reaches into a descendant (no `> .ippon-child` selector). + +An example with a `dropdown` organism that ionizes the `button` atom it contains: + +The container carries the ion (in the `dropdown` mixin): + +```html +
+ +
+``` + +The atom declares its ionized behavior (in `atom/button/_button.scss`, **not** in the organism): + +```scss +.ippon-button { + // button styles + + .ippon-dropdown---buttons & { + justify-content: flex-start; + width: 100%; + } +} +``` + +Note: an ion is a naming convention, unrelated to the `atom/ion` component (which renders Ionicons icons). + ## Tikui To document with Tikui, simply include the component's Markdown file in the file where you want to document it. diff --git a/AGENTS.md b/AGENTS.md index 37cdb27..3caf651 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,7 +24,7 @@ Run from the **monorepo root** unless noted otherwise. | Format (check only) | `mise format-ci` | | Lint (fix) | `mise lint` | | Lint (check only) | `mise lint-ci` | -| Unit tests (not interactive) | `mise unit-test-ci` | +| Unit tests (not interactive) | `mise test-unit-ci` | ## Pattern Library (`styles`) diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..03de627 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,22 @@ +# Changelog + +All notable changes to the Ippon UI packages are documented in this file, so consumers can see what happens between releases. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), with one entry per release listing the affected package versions. + +## 2026-07-03 — @ippon-ui/styles 0.0.7 · @ippon-ui/react 0.0.6 + +### Added + +- `dropdown` organism: floating action panel built on the native popover API, opened through a button using `command`/`commandfor`, anchored to its trigger and flipping to stay within the viewport. +- `separator` atom: thin dividing line, used to group dropdown items. +- `IpponDropdown` React component, behavior-free, forwarding `onKeyDown` and `onToggle`; the `KeyboardNavigation` Storybook story shows how to wire a select-like keyboard flow. +- `IpponSeparator` React component. +- `popoverTarget` and `popoverTargetAction` props on `IpponButton` to trigger popovers without JavaScript. +- Ion convention (`component---ion`): a class set on a container that ionizes descendants, each component declaring its own ionized behavior (see the Pattern Library documentation). + +### Changed + +- Buttons apply their hover style on `:focus-visible`, making keyboard focus as visible as mouse hover. + +Changes released before this file was introduced are not listed here. diff --git a/react/package.json b/react/package.json index 51ffa82..14fc769 100644 --- a/react/package.json +++ b/react/package.json @@ -1,7 +1,7 @@ { "name": "@ippon-ui/react", "description": "Ippon UI React Component Library", - "version": "0.0.5", + "version": "0.0.6", "license": "Apache-2.0", "repository": { "type": "git", @@ -15,7 +15,8 @@ "build": "pnpm run build:lib && pnpm run build:storybook", "build:lib": "tsc -b && vite build", "build:storybook": "storybook build", - "lint": "eslint .", + "lint": "eslint . --fix", + "lint:ci": "eslint .", "preview": "vite preview", "test:unit": "vitest", "test:unit:ci": "vitest run" @@ -38,7 +39,7 @@ }, "peerDependencies": { "@ippon-ui/icons": "~0.0.2", - "@ippon-ui/styles": "~0.0.5", + "@ippon-ui/styles": "~0.0.7", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/react/src/IpponButton.tsx b/react/src/IpponButton.tsx index cd8ff79..85a8040 100644 --- a/react/src/IpponButton.tsx +++ b/react/src/IpponButton.tsx @@ -25,6 +25,8 @@ type IpponButtonVanillaProps = { iconLeft?: IpponButtonIcon; iconRight?: IpponButtonIcon; onClick?: () => void | Promise; + popoverTarget?: string; + popoverTargetAction?: 'toggle' | 'show' | 'hide'; }; export type IpponButtonProps = DataSelectableWithChildren; @@ -79,6 +81,8 @@ export const IpponButton = (props: IpponButtonProps) => { disabled={isDisabled} aria-busy={loading || undefined} data-selector={props.dataSelector} + popoverTarget={props.popoverTarget} + popoverTargetAction={props.popoverTargetAction} onClick={handleClick} > diff --git a/react/src/IpponDropdown.tsx b/react/src/IpponDropdown.tsx new file mode 100644 index 0000000..5d3f8da --- /dev/null +++ b/react/src/IpponDropdown.tsx @@ -0,0 +1,23 @@ +import { clsx } from 'clsx'; +import type { KeyboardEventHandler, ToggleEventHandler } from 'react'; +import type { DataSelectableWithChildren } from './DataSelectable.ts'; + +type IpponDropdownProps = DataSelectableWithChildren<{ + id: string; + className?: string; + onKeyDown?: KeyboardEventHandler; + onToggle?: ToggleEventHandler; +}>; + +export const IpponDropdown = (props: IpponDropdownProps) => ( +
+ {props.children} +
+); diff --git a/react/src/IpponSeparator.tsx b/react/src/IpponSeparator.tsx new file mode 100644 index 0000000..210e6fd --- /dev/null +++ b/react/src/IpponSeparator.tsx @@ -0,0 +1,10 @@ +import { clsx } from 'clsx'; +import type { DataSelectable } from './DataSelectable.ts'; + +type IpponSeparatorProps = DataSelectable<{ + className?: string; +}>; + +export const IpponSeparator = (props: IpponSeparatorProps) => ( +
+); diff --git a/react/src/index.ts b/react/src/index.ts index 22c3010..c048a3c 100644 --- a/react/src/index.ts +++ b/react/src/index.ts @@ -3,6 +3,7 @@ export { IpponButton } from './IpponButton.tsx'; export { IpponButtonCard } from './IpponButtonCard.tsx'; export { IpponCard } from './IpponCard.tsx'; export { IpponContainer } from './IpponContainer.tsx'; +export { IpponDropdown } from './IpponDropdown.tsx'; export { IpponGrid, IpponGridSlot } from './IpponGrid.tsx'; export { IpponHSpace, IpponHSpaceSlot } from './IpponHSpace.tsx'; export { IpponIcon } from './IpponIcon.tsx'; @@ -10,6 +11,7 @@ export { IpponImportFile } from './IpponImportFile.tsx'; export { IpponIon } from './IpponIon.tsx'; export { IpponMeter } from './IpponMeter.tsx'; export { IpponProgress } from './IpponProgress.tsx'; +export { IpponSeparator } from './IpponSeparator.tsx'; export { IpponText } from './IpponText.tsx'; export { IpponTitle } from './IpponTitle.tsx'; export { IpponVSpace, IpponVSpaceSlot } from './IpponVSpace.tsx'; diff --git a/react/stories/IpponDropdown.stories.tsx b/react/stories/IpponDropdown.stories.tsx new file mode 100644 index 0000000..c4bf2db --- /dev/null +++ b/react/stories/IpponDropdown.stories.tsx @@ -0,0 +1,115 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import type { KeyboardEvent, ToggleEvent } from 'react'; +import { IpponDropdown } from '../src/IpponDropdown.tsx'; +import { IpponButton } from '../src/IpponButton.tsx'; +import { IpponSeparator } from '../src/IpponSeparator.tsx'; + +const moveFocusOnArrowKey = (event: KeyboardEvent) => { + if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') { + return; + } + const buttons = [ + ...event.currentTarget.querySelectorAll('button:not(:disabled)'), + ]; + if (buttons.length === 0) { + return; + } + const index = buttons.indexOf(document.activeElement as HTMLButtonElement); + const delta = event.key === 'ArrowDown' ? 1 : -1; + buttons[(index + delta + buttons.length) % buttons.length].focus(); + event.preventDefault(); +}; + +const focusFirstButtonOnOpen = (event: ToggleEvent) => { + if (event.newState !== 'open') { + return; + } + event.currentTarget.querySelector('button:not(:disabled)')?.focus(); +}; + +const meta = { + title: 'Organism/Dropdown', + component: IpponDropdown, + args: { + id: 'dropdown', + }, + argTypes: { + children: { control: false }, + onKeyDown: { control: false }, + onToggle: { control: false }, + }, + render: (args) => ( + <> + + Dropdown + + + + Item + + + + Destructive item + + + + ), +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const KeyboardNavigation: Story = { + args: { + id: 'dropdown-keyboard', + onKeyDown: moveFocusOnArrowKey, + onToggle: focusFirstButtonOnOpen, + }, + parameters: { + docs: { + description: { + story: + 'The dropdown ships no keyboard behavior: it only forwards `onKeyDown` and `onToggle` to the panel. To get a select-like flow — focus the first item when the popover opens, then move with the arrow keys (wrap-around, disabled buttons skipped) — wire your own handlers:', + }, + source: { + language: 'tsx', + code: `const moveFocusOnArrowKey = (event: KeyboardEvent) => { + if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') { + return; + } + const buttons = [ + ...event.currentTarget.querySelectorAll('button:not(:disabled)'), + ]; + if (buttons.length === 0) { + return; + } + const index = buttons.indexOf(document.activeElement as HTMLButtonElement); + const delta = event.key === 'ArrowDown' ? 1 : -1; + buttons[(index + delta + buttons.length) % buttons.length].focus(); + event.preventDefault(); +}; + +const focusFirstButtonOnOpen = (event: ToggleEvent) => { + if (event.newState !== 'open') { + return; + } + event.currentTarget.querySelector('button:not(:disabled)')?.focus(); +}; + + + + Item + +;`, + }, + }, + }, +}; diff --git a/react/test/IpponButton.spec.tsx b/react/test/IpponButton.spec.tsx index a6363fb..bc1db5f 100644 --- a/react/test/IpponButton.spec.tsx +++ b/react/test/IpponButton.spec.tsx @@ -539,6 +539,30 @@ describe('IpponButton', () => { }); }); + describe('Popover trigger', () => { + it('should not set popover attributes by default', () => { + render(Default); + + const button = getIpponButton(); + + expect(button).not.toHaveAttribute('popovertarget'); + expect(button).not.toHaveAttribute('popovertargetaction'); + }); + + it('should set popover target and action', () => { + render( + + Open + , + ); + + const button = getIpponButton(); + + expect(button).toHaveAttribute('popovertarget', 'menu'); + expect(button).toHaveAttribute('popovertargetaction', 'toggle'); + }); + }); + describe('Combinations', () => { it('should combine color, variant and size', () => { render( diff --git a/react/test/IpponDropdown.spec.tsx b/react/test/IpponDropdown.spec.tsx new file mode 100644 index 0000000..3ad45ab --- /dev/null +++ b/react/test/IpponDropdown.spec.tsx @@ -0,0 +1,56 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { render, screen, configure, cleanup, fireEvent, createEvent } from '@testing-library/react'; +import '@testing-library/jest-dom/vitest'; +import { IpponDropdown } from '../src'; + +configure({ + testIdAttribute: 'data-selector', +}); + +const getIpponDropdown = () => screen.getByTestId('ippon-dropdown'); + +describe('IpponDropdown', () => { + afterEach(cleanup); + + it('should be like pattern library', () => { + render( + + Content + , + ); + + const dropdown = getIpponDropdown(); + + expect(dropdown).toHaveClass('ippon-dropdown', 'ippon-dropdown---buttons'); + expect(dropdown).toHaveAttribute('id', 'menu'); + expect(dropdown).toHaveAttribute('popover', 'auto'); + expect(dropdown).toHaveTextContent('Content'); + }); + + it('should merge additional className', () => { + render(); + + expect(getIpponDropdown()).toHaveClass('-custom'); + }); + + it('should call onKeyDown', () => { + const onKeyDown = vi.fn(); + + render(); + + fireEvent.keyDown(getIpponDropdown(), { key: 'ArrowDown' }); + + expect(onKeyDown).toHaveBeenCalled(); + }); + + it('should call onToggle', () => { + const onToggle = vi.fn(); + + render(); + + const dropdown = getIpponDropdown(); + fireEvent(dropdown, Object.assign(createEvent('toggle', dropdown), { newState: 'open' })); + + expect(onToggle).toHaveBeenCalled(); + }); +}); diff --git a/react/test/IpponSeparator.spec.tsx b/react/test/IpponSeparator.spec.tsx new file mode 100644 index 0000000..57c005f --- /dev/null +++ b/react/test/IpponSeparator.spec.tsx @@ -0,0 +1,29 @@ +import { afterEach, describe, expect, it } from 'vitest'; +import { render, screen, configure, cleanup } from '@testing-library/react'; +import '@testing-library/jest-dom/vitest'; +import { IpponSeparator } from '../src'; + +configure({ + testIdAttribute: 'data-selector', +}); + +const getIpponSeparator = () => screen.getByTestId('ippon-separator'); + +describe('IpponSeparator', () => { + afterEach(cleanup); + + it('should be like pattern library', () => { + render(); + + const separator = getIpponSeparator(); + + expect(separator).toHaveClass('ippon-separator'); + expect(separator.tagName).toBe('HR'); + }); + + it('should merge additional className', () => { + render(); + + expect(getIpponSeparator()).toHaveClass('ippon-separator', '-custom'); + }); +}); diff --git a/react/tsconfig.app.json b/react/tsconfig.app.json index d034c66..922dadf 100644 --- a/react/tsconfig.app.json +++ b/react/tsconfig.app.json @@ -2,7 +2,7 @@ "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "target": "es2023", - "lib": ["ES2023", "DOM"], + "lib": ["ES2023", "DOM", "DOM.Iterable"], "module": "esnext", "types": ["vite/client"], "skipLibCheck": true, diff --git a/react/tsconfig.json b/react/tsconfig.json index d32ff68..f8d91ca 100644 --- a/react/tsconfig.json +++ b/react/tsconfig.json @@ -1,4 +1,8 @@ { "files": [], - "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }] + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.storybook.json" } + ] } diff --git a/styles/.stylelintrc.json b/styles/.stylelintrc.json index 87121ba..dba18ce 100644 --- a/styles/.stylelintrc.json +++ b/styles/.stylelintrc.json @@ -4,7 +4,7 @@ "rules": { "max-nesting-depth": 3, "custom-property-pattern": "^_?[a-zA-Z][a-zA-Z0-9\\-]*$", - "selector-class-pattern": "^-?([a-z][a-z0-9]*)(-[a-z0-9]+)*$", + "selector-class-pattern": "^-?([a-z][a-z0-9]*)(-{1,3}[a-z0-9]+)*$", "no-descending-specificity": null, "selector-no-qualifying-type": [ true, diff --git a/styles/package.json b/styles/package.json index 59a0669..4c109b9 100644 --- a/styles/package.json +++ b/styles/package.json @@ -1,6 +1,6 @@ { "name": "@ippon-ui/styles", - "version": "0.0.6", + "version": "0.0.7", "description": "Ippon UI Pattern Library", "repository": { "type": "git", diff --git a/styles/src/atom/_atom.scss b/styles/src/atom/_atom.scss index 6e16f64..7315a0f 100644 --- a/styles/src/atom/_atom.scss +++ b/styles/src/atom/_atom.scss @@ -3,6 +3,7 @@ @use 'icon/icon'; @use 'meter/meter'; @use 'progress/progress'; +@use 'separator/separator'; @use 'tab/tab'; @use 'text/text'; @use 'title/title'; diff --git a/styles/src/atom/atom.pug b/styles/src/atom/atom.pug index c25611a..20cc74d 100644 --- a/styles/src/atom/atom.pug +++ b/styles/src/atom/atom.pug @@ -24,6 +24,8 @@ block content include:componentDoc(height=150) meter/meter.md .tikui-vertical-spacing--line include:componentDoc(height=150) progress/progress.md + .tikui-vertical-spacing--line + include:componentDoc(height=40) separator/separator.md .tikui-vertical-spacing--line include:componentDoc(height=60) tab/tab.md .tikui-vertical-spacing--line diff --git a/styles/src/atom/button/_button.scss b/styles/src/atom/button/_button.scss index 0fc6756..d309766 100644 --- a/styles/src/atom/button/_button.scss +++ b/styles/src/atom/button/_button.scss @@ -85,7 +85,7 @@ --_icon-size: var(--ippon-size-24); } - &:hover { + &:is(:hover, :focus-visible) { --_background-color: var(--_color-primary-hover); --_border-color: var(--_color-primary-hover); } @@ -108,7 +108,7 @@ --_border-color: var(--_color-secondary); --_color: var(--_color-text-secondary); - &:hover { + &:is(:hover, :focus-visible) { --_background-color: var(--_color-secondary-hover); --_border-color: var(--_color-secondary-hover); } @@ -130,7 +130,7 @@ --_color: var(--_color-text-secondary); --_border-color: var(--_color-border); - &:hover { + &:is(:hover, :focus-visible) { --_background-color: var(--_color-secondary-hover); } @@ -150,7 +150,7 @@ --_border-color: transparent; --_color: var(--_color-text-secondary); - &:hover { + &:is(:hover, :focus-visible) { --_background-color: var(--_color-secondary-hover); --_border-color: transparent; } @@ -239,4 +239,9 @@ --_color-text-secondary: var(--ippon-color-neutral-text-icon-on-secondary); --_color-border: var(--ippon-color-neutral-border); } + + .ippon-dropdown---buttons & { + justify-content: flex-start; + width: 100%; + } } diff --git a/styles/src/atom/separator/_separator.scss b/styles/src/atom/separator/_separator.scss new file mode 100644 index 0000000..ec23c80 --- /dev/null +++ b/styles/src/atom/separator/_separator.scss @@ -0,0 +1,5 @@ +.ippon-separator { + margin: 0; + border: 0; + border-top: 1px solid var(--ippon-color-neutral-border); +} diff --git a/styles/src/atom/separator/separator.code.pug b/styles/src/atom/separator/separator.code.pug new file mode 100644 index 0000000..a888156 --- /dev/null +++ b/styles/src/atom/separator/separator.code.pug @@ -0,0 +1,3 @@ +include separator.mixin.pug + ++ippon-separator diff --git a/styles/src/atom/separator/separator.md b/styles/src/atom/separator/separator.md new file mode 100644 index 0000000..d40f0a6 --- /dev/null +++ b/styles/src/atom/separator/separator.md @@ -0,0 +1,3 @@ +## Separator + +Thin dividing line rendered as an `hr` (implicit `separator` role). It stays flush so the surrounding component controls its spacing, and is reused to group content such as items inside a dropdown. diff --git a/styles/src/atom/separator/separator.mixin.pug b/styles/src/atom/separator/separator.mixin.pug new file mode 100644 index 0000000..3983565 --- /dev/null +++ b/styles/src/atom/separator/separator.mixin.pug @@ -0,0 +1,2 @@ +mixin ippon-separator + hr.ippon-separator diff --git a/styles/src/atom/separator/separator.render.pug b/styles/src/atom/separator/separator.render.pug new file mode 100644 index 0000000..718b30e --- /dev/null +++ b/styles/src/atom/separator/separator.render.pug @@ -0,0 +1,4 @@ +extends /layout + +block body + include separator.code.pug diff --git a/styles/src/organism/_organism.scss b/styles/src/organism/_organism.scss index f04e034..1472251 100644 --- a/styles/src/organism/_organism.scss +++ b/styles/src/organism/_organism.scss @@ -1,6 +1,7 @@ @use 'button-card/button-card'; @use 'card/card'; @use 'container/container'; +@use 'dropdown/dropdown'; @use 'grid/grid'; @use 'header/header'; @use 'h-space/h-space'; diff --git a/styles/src/organism/dropdown/_dropdown.scss b/styles/src/organism/dropdown/_dropdown.scss new file mode 100644 index 0000000..5cdc94c --- /dev/null +++ b/styles/src/organism/dropdown/_dropdown.scss @@ -0,0 +1,17 @@ +.ippon-dropdown { + flex-direction: column; + gap: var(--ippon-size-4); + margin: var(--ippon-size-8) 0 0; + border: 0; + border-radius: var(--ippon-radius-l); + box-shadow: var(--ippon-shadow-l2); + background-color: var(--ippon-color-neutral-surface-primary); + padding: var(--ippon-size-8); + color: var(--ippon-color-neutral-text-icon-on-primary); + position-area: block-end span-inline-end; + position-try-fallbacks: flip-block, flip-inline; + + &:popover-open { + display: flex; + } +} diff --git a/styles/src/organism/dropdown/dropdown.code.pug b/styles/src/organism/dropdown/dropdown.code.pug new file mode 100644 index 0000000..bf33e43 --- /dev/null +++ b/styles/src/organism/dropdown/dropdown.code.pug @@ -0,0 +1,9 @@ +include /atom/button/button.mixin.pug +include /atom/separator/separator.mixin.pug +include dropdown.mixin.pug + ++ippon-button({ command: 'toggle-popover', commandfor: 'dropdown', variant: 'outline', iconRight: { name: 'chevron-down' } }) Dropdown ++ippon-dropdown({ id: 'dropdown' }) + +ippon-button({ variant: 'text', color: 'neutral', iconLeft: { name: 'ellipse' } }) Item + +ippon-separator + +ippon-button({ variant: 'text', color: 'error', iconLeft: { name: 'trash' } }) Destructive item diff --git a/styles/src/organism/dropdown/dropdown.md b/styles/src/organism/dropdown/dropdown.md new file mode 100644 index 0000000..14cfc0e --- /dev/null +++ b/styles/src/organism/dropdown/dropdown.md @@ -0,0 +1,3 @@ +## Dropdown + +Floating panel of actions anchored to a trigger, built on the native popover API. Pair it with a button using `command="toggle-popover"` and `commandfor` pointing to the panel `id`; the trigger becomes the implicit anchor. Buttons placed inside are ionized full-width through `ippon-dropdown---buttons`, and an `ippon-separator` can group them. diff --git a/styles/src/organism/dropdown/dropdown.mixin.pug b/styles/src/organism/dropdown/dropdown.mixin.pug new file mode 100644 index 0000000..86e3073 --- /dev/null +++ b/styles/src/organism/dropdown/dropdown.mixin.pug @@ -0,0 +1,4 @@ +mixin ippon-dropdown(options) + - const { id } = options || {}; + .ippon-dropdown.ippon-dropdown---buttons(popover, id=id) + block diff --git a/styles/src/organism/dropdown/dropdown.render.pug b/styles/src/organism/dropdown/dropdown.render.pug new file mode 100644 index 0000000..8aec1df --- /dev/null +++ b/styles/src/organism/dropdown/dropdown.render.pug @@ -0,0 +1,4 @@ +extends /layout + +block body + include dropdown.code.pug diff --git a/styles/src/organism/organism.pug b/styles/src/organism/organism.pug index 92d5bbe..d2f69cd 100644 --- a/styles/src/organism/organism.pug +++ b/styles/src/organism/organism.pug @@ -18,6 +18,8 @@ block content include:componentDoc(height=280) card/card.md .tikui-vertical-spacing--line include:componentDoc(height=110) container/container.md + .tikui-vertical-spacing--line + include:componentDoc(height=200) dropdown/dropdown.md .tikui-vertical-spacing--line include:componentDoc header/header.md .tikui-vertical-spacing--line